[x] xiaoyu.sh

XIAOYU(1) — THE COMPLETE MANUAL

A reference to keep.

xiaoyu
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣤⣤⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⡿⠿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⠋⢀⣠⣾⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡿⣡⣾⣿⣿⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣰⣿⣷⠿⠟⠛⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⢀⣠⣤⣶⣶⣦⣤⡀⢿⣿⡿⣿⣥⣤⣂⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⣠⣶⣿⣿⣿⠟⠛⠛⣻⣿⣿⣏⠁⠀⠹⣿⣿⣿⣷⣦⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠻⣿⣿⠟⠁⢀⣠⣾⣿⡿⣿⣿⣄⠀⠀⠉⠙⠛⠿⠿⠿⠿⠽⢽⣿⣿⡿⢿⣿⣿⣿⣶⣄⠀⠀⠀
⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⡟⠀⣿⣿⣿⣷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⢿⣿⣿⣧⠀⠀
⠀⠀⠀⠀⢻⣿⣿⣿⣿⠟⠁⠀⢸⣿⡿⠛⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⡀⠀
⠀⠀⠀⠀⠀⠉⠚⠋⠀⠀⢀⣠⣾⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣷⠀
⠀⠀⢀⣠⣴⣶⣶⣶⣿⣿⣿⣿⡿⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣿⡟⠋⠓
⠀⣴⣿⠇⠉⠉⠛⠛⠙⠛⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁⠃⠀⠀
⣴⣿⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⢻⣿⣿⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠳⡏⠋⠉⠂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
Static sites, tied together. · xiaoyu.sh
What it does
xiaoyu is a single command-line binary that puts a folder of static files
online at your own domain, over HTTPS, on AWS.
Point it at a directory, tell it your domain, and it creates and maintains
everything needed to serve that directory:
• a private S3 bucket (never public, versioned) for the files;
• a CloudFront distribution that reads S3 through Origin Access Control,
  with compression, HTTP→HTTPS redirect, TLS 1.2+, and AWS's managed security
  headers;
• a TLS certificate from ACM in us-east-1, validated by DNS;
• atomic releases: each deploy uploads a new immutable, content-addressed
  release, and CloudFront switches to it only once every byte is in place, so
  visitors never see a half-updated site — and you can roll back;
• verification: after deploying, it can fetch your live HTTPS URLs and
  compare their SHA-256 with your local files;
• aliases and redirects: extra hostnames like www., all redirected to
  one canonical host, plus directory URLs or SPA routing.
It also creates a dedicated, least-privilege IAM user for day-to-day deploys,
so your admin credentials are only needed once.
There is no AWS CLI, no Node.js, no Terraform, no daemon, no database. One
binary, one small YAML file.
Requirements: an AWS account (commercial regions only — China and GovCloud
are rejected), a domain you control DNS for, and Go 1.25+ if you build from
source.
Install
Download a prebuilt binary for macOS or Linux from
xiaoyu.sh, verify it against the published
SHA256SUMS.txt, and put it on your PATH. The builds are unsigned, so macOS
quarantines them: clear it with xattr -c xiaoyu after extracting.
Or build for your machine:
sh
make build
./bin/xiaoyu version
Or install with Go 1.25 or newer:
sh
go install github.com/klodio/xiaoyu/cmd/xiaoyu@latest
Cross-compile for another machine, for example a Linux CI runner:
sh
GOOS=linux GOARCH=amd64 go build -trimpath -o xiaoyu-linux-amd64 ./cmd/xiaoyu
Copying that one executable to another machine is enough. That machine needs
only system CA certificates and AWS credentials from the standard AWS SDK
chain (environment variables, a profile/SSO session, web identity, or an
instance/task role).
Quick start
Run everything from the folder that contains your website files.
1. Create the configuration
sh
xiaoyu init
An interactive wizard explains every setting, recommends a default, validates
your answers, and writes .xiaoyu.yaml after you confirm. It never asks for
secrets. Prefer editing by hand? Copy
`.xiaoyu.example.yaml` to .xiaoyu.yaml instead.
Then check it — this is offline and needs no credentials:
sh
xiaoyu config check
2. Get provisioning credentials
AWS will not let an unauthenticated tool create an IAM user, so the first-time
setup needs an identity that already exists. Root credentials are refused
either way. Pick one of these:
A. Launch the bootstrap stack (no IAM work by hand). The launch link on
xiaoyu.sh opens CloudFormation with
`cloudformation/bootstrap.yaml`, which creates
one temporary IAM user scoped to this account and outputs its access key. Then
hand that key to xiaoyu:
sh
xiaoyu login
login prompts for the key (the secret is not echoed), proves it works with
sts:GetCallerIdentity, and offers to save it with mode 0600 outside your
site folder. --no-save verifies without writing anything.
The stack's identity can create this site's infrastructure and the runtime IAM
user — it is scoped, not harmless. Delete the stack once step 5 finishes;
that deletes the user and revokes the key. It deliberately cannot run
xiaoyu destroy. The stack is created in ap-northeast-2, where the template
is hosted; that is unrelated to your site's region.
B. Use your own administrator credentials, ideally temporary ones from an
admin role:
sh
export AWS_ACCESS_KEY_ID='temporary-bootstrap-key'
export AWS_SECRET_ACCESS_KEY='temporary-bootstrap-secret'
export AWS_SESSION_TOKEN='temporary-bootstrap-token'   # if applicable
xiaoyu login accepts these too, if you would rather keep them in a file than
in your shell history.
3. Provision the site
sh
xiaoyu bootstrap-user --deploy
The first run normally stops with exit code 2: ACM is waiting for DNS
validation. It prints the exact CNAME record to add. No IAM user or access key
is created yet.
4. Add the certificate-validation DNS record
Add the printed CNAME at your DNS provider. If you use Namecheap, the tool can
do it for you (see Namecheap DNS). Keep this record forever
so ACM can renew the certificate automatically.
5. Finish provisioning and get your deploy credentials
Once ACM reports ISSUED, run the same command again:
sh
xiaoyu bootstrap-user --deploy
It finishes CloudFront, publishes the first release, then creates an IAM user
scoped to exactly this site's bucket, certificate, distribution, OAC, and
routing function. It verifies that user's new key actually works, writes it
with mode 0600 to your OS user-config directory (outside your website
folder), and prints the path.
Finally, add the printed site DNS record (ALIAS/CNAME to CloudFront) for each
domain and alias.
6. Use the dedicated credentials from now on
sh
source '/path/printed/by/xiaoyu/example.com-xxxxxxxxxxxx.env'
xiaoyu status
That file exports the AWS key plus the site settings that were in effect, so a
fresh shell is enough to deploy. Keep it private; it holds a long-lived secret.
7. Verify the live site
sh
xiaoyu verify --wait         # index plus one sample file
xiaoyu verify --all --wait   # every file
Verification downloads your public HTTPS URLs and compares SHA-256 digests
with the local files, checking every alias redirect on the way. --wait waits
for CloudFront to finish deploying and invalidating first.
Leave verify_after_deploy: false until DNS resolves; then turn it on so
every deploy checks itself.
That's it. The everyday loop afterwards is: edit files → xiaoyu deploy.
Everyday use
sh
xiaoyu deploy                  # publish the current folder as a new release
xiaoyu upload                  # upload all files (no AWS config reconciliation)
xiaoyu upload --file app.css   # upload a single file
xiaoyu sync --dry-run          # show exactly what would change
xiaoyu sync                    # publish changes
xiaoyu sync --delete --yes     # also drop files that no longer exist locally
upload and sync never delete anything unless you pass --delete. With
atomic_deploy: true (the default) all three create an immutable release and
switch CloudFront only when it is complete.
Changed files are detected by SHA-256, stored as object metadata — not by
timestamps or ETags. Unchanged files are copied server-side rather than
re-uploaded.
If you changed settings that affect AWS itself (new alias, new routing mode,
changed security headers), the dedicated user is intentionally not allowed to
do that. Restore admin credentials and run:
sh
xiaoyu bootstrap-user --provision
which reprovisions and refreshes the user's policy without rotating its key.
Configuration
.xiaoyu.yaml in the current directory holds non-secret settings.
XIAOYU_CONFIG selects a different file. Every setting has an environment
override, which wins over the YAML value. Unknown YAML keys are rejected.
yaml
domain: example.com
aliases:
  - www.example.com
canonical_domain: example.com
region: eu-west-1
source: .
routing: directory
┌─────────────────────┬─────────────────────────────────┬──────────────────────────────────┐
│ YAML                │ Environment override            │ Default                          │
├─────────────────────┼─────────────────────────────────┼──────────────────────────────────┤
│ domain              │ XIAOYU_DOMAIN                   │ required                         │
│ aliases             │ XIAOYU_ALIASES                  │ none                             │
│                     │ (comma-separated)               │                                  │
│ canonical_domain    │ XIAOYU_CANONICAL_DOMAIN         │ primary domain                   │
│ region              │ AWS_REGION                      │ required                         │
│ source              │ XIAOYU_SOURCE_DIR               │ current directory                │
│ bucket              │ XIAOYU_BUCKET                   │ deterministic account/domain     │
│                     │                                 │ name                             │
│ routing             │ XIAOYU_ROUTING                  │ files                            │
│ security_headers    │ XIAOYU_SECURITY_HEADERS         │ true                             │
│ atomic_deploy       │ XIAOYU_ATOMIC_DEPLOY            │ true                             │
│ retain_releases     │ XIAOYU_RETAIN_RELEASES          │ 5                                │
│ verify_after_deploy │ XIAOYU_VERIFY_AFTER_DEPLOY      │ false                            │
│ verify_all_files    │ XIAOYU_VERIFY_ALL_FILES         │ false                            │
│ index_document      │ XIAOYU_INDEX_DOCUMENT           │ index.html                       │
│ error_document      │ XIAOYU_ERROR_DOCUMENT           │ 404.html when present            │
│ price_class         │ XIAOYU_PRICE_CLASS              │ PriceClass_100                   │
│ asset_cache_control │ XIAOYU_CACHE_CONTROL            │ public,max-age=300               │
│ html_cache_control  │ XIAOYU_HTML_CACHE_CONTROL       │ no-cache                         │
│ exclude             │ XIAOYU_EXCLUDE                  │ none                             │
│                     │ (comma-separated)               │                                  │
│ log_bucket          │ XIAOYU_LOG_BUCKET               │ disabled                         │
│ log_prefix          │ XIAOYU_LOG_PREFIX               │ empty                            │
└─────────────────────┴─────────────────────────────────┴──────────────────────────────────┘
Exclude patterns use Go path.Match. Some things are never uploaded, whatever
you configure: .git, .DS_Store, .env*, .xiaoyu.env*, .xiaoyu.yaml,
.xiaoyu.yml, .release.json, the xiaoyu/xiaoyu.exe binaries, the
the tool's internal _xiaoyu/ and _s3site/ namespaces, Open Design directories (.od/, .od-skills/,
open-design/, .open-design/), symlinks, and non-regular files. Similarly
named public assets such as xiaoyu.css upload normally.
Routing
• files — direct object paths only; / serves index_document.
• directory — /about and /about/ serve /about/index.html.
• spa — extensionless paths serve the root index_document.
Rewrites and canonical-host redirects run in one managed CloudFront Function.
Every alias goes into both the certificate and the distribution; requests to a
non-canonical hostname get a 301 that preserves path and query string.
directory and spa reject extensionless source files whose public paths the
rewrite would capture.
Commands
┌────────────────────────────┬─────────────────────────────────────────────────────────────┐
│ Command                    │ Purpose                                                     │
├────────────────────────────┼─────────────────────────────────────────────────────────────┤
│ xiaoyu init                │ Interactive configuration wizard (alias of config init)     │
│ xiaoyu config check        │ Validate local configuration and files, offline             │
│ xiaoyu login               │ Verify pasted provisioning credentials and store them       │
│                            │ privately                                                   │
│ xiaoyu bootstrap-user      │ Provision AWS and create/rotate/repair the dedicated user   │
│ xiaoyu deploy              │ Reconcile AWS settings and publish a release                │
│ xiaoyu plan                │ Preview AWS and content changes, read-only                  │
│ xiaoyu status              │ AWS, DNS, HTTPS, release, and 24h CloudWatch metrics        │
│ xiaoyu doctor              │ Diagnose local, AWS, DNS, HTTPS, redirect, and content      │
│                            │ health                                                      │
│ xiaoyu upload              │ Upload all files, or one with --file                        │
│ xiaoyu sync                │ Checksum sync, optionally with --delete                     │
│ xiaoyu verify              │ Compare live HTTPS responses with local SHA-256             │
│ xiaoyu releases            │ List releases, or --prune --yes old ones                    │
│ xiaoyu rollback --yes <id> │ Point CloudFront back at a retained release                 │
│ xiaoyu dns                 │ Plan or apply Namecheap DNS records                         │
│ xiaoyu destroy             │ Plan, or confirm, guarded teardown                          │
│ xiaoyu version             │ Print the version                                           │
└────────────────────────────┴─────────────────────────────────────────────────────────────┘
status, releases, rollback, dns, and destroy work without the local
website folder. plan, deploy, upload, sync, verify, and
bootstrap-user --deploy need it.
Run xiaoyu with no arguments for the full usage text.
Releases and rollback
Each release is stored under a content-addressed prefix:
_xiaoyu/releases/<release-id>/
Sites provisioned before the rename keep _s3site/ instead; both are read, and
neither is ever published.
CloudFront's origin path is switched only after the complete release and its
manifest are verified in S3. Old releases stay immutable, so rollback is just
a pointer change:
sh
xiaoyu releases                      # list, with integrity status
xiaoyu rollback --yes <release-id>   # go back
xiaoyu releases --prune --yes        # delete inactive ones beyond retain_releases
Pruning keeps every release younger than one hour, re-checks the active
release before each deletion, and waits for CloudFront and its invalidations
to settle. Deploy, rollback, prune, and destroy coordinate through a renewable
S3 lease so two of them can never act on a half-written release.
Checking a site
sh
xiaoyu plan      # what would change, read-only
xiaoyu status    # AWS/DNS/HTTPS state and CloudFront metrics
xiaoyu doctor    # actionable health checks, including a live byte comparison
xiaoyu verify    # SHA-256 check of the live site
For subdomains, DNS status requires an exact CNAME match to the distribution.
At an apex domain, ALIAS/ANAME flattening hides the target, so a successful
lookup is reported as indeterminate rather than as proof.
Namecheap DNS
Every command that needs DNS prints records you can add by hand. If your
domain is at Namecheap, automation is available and always plan-first:
sh
export NAMECHEAP_API_USER='...'
export NAMECHEAP_API_KEY='...'
export NAMECHEAP_USERNAME='...'
export NAMECHEAP_CLIENT_IP='your-whitelisted-public-ip'

xiaoyu dns                 # show the plan
xiaoyu dns --apply --yes   # apply it
It reads your complete current record set, replaces only conflicting records
for the hosts it needs, keeps everything else (MX, TXT, …), and re-reads all
records immediately before writing so it aborts if anything changed since the
plan. Namecheap has no conditional-write token, so a small unavoidable race
remains. All aliases must currently share one registrable domain; manual
instructions support aliases in different zones.
Access logs
Set log_bucket to enable CloudFront standard (legacy) access logging:
yaml
log_bucket: my-cloudfront-log-bucket
log_prefix: example.com/
The bucket must already exist, be separate from the site bucket, and be
configured for CloudFront log delivery. Legacy logging needs bucket-ACL
permissions, so it does not work with the dedicated runtime user — use it
only if you deploy under an administrator-managed role. The site bucket's
private, bucket-owner-enforced configuration is never weakened for logging.
Tearing down
destroy is plan-only unless you confirm:
sh
xiaoyu destroy                       # show what would be deleted
xiaoyu destroy --retain-bucket --yes # delete the edge, keep the bucket
xiaoyu destroy --yes                 # delete everything it owns
It only touches resources whose ownership tags and private ownership marker
prove they belong to this site, and re-checks that immediately before each
deletion. Namecheap DNS records and the IAM user are left behind and reported
so you can clean them up deliberately. The dedicated deploy user has no
deletion rights — run destroy with administrator credentials.
After --retain-bucket, the bucket and all its releases remain but the
distribution, OAC, certificate, and routing functions are gone. To bring the
site back, restore the same configuration and administrator credentials, keep
XIAOYU_IAM_USER pointing at the existing user, and run:
sh
xiaoyu bootstrap-user --provision
This rebuilds the edge around the retained release without needing your local
files at all. If the retained release is missing or unverifiable, it fails
rather than silently publishing something else. You may need to complete a new
ACM validation and rerun the command.
How it stays safe
A few design choices worth knowing:
• S3 is never public. The public site is CloudFront, which signs its
  origin requests with Origin Access Control. The S3 website endpoint is never
  enabled. The bucket policy requires TLS, requests are bound to the expected
  AWS account, and deployment refuses to preserve unexpected access-granting
  policy statements.
• Admin credentials are temporary. They provision infrastructure once; the
  long-lived user created afterwards is scoped to the exact ARNs of this site
  and can only deploy content. It cannot create certificates, distributions,
  IAM resources, or delete infrastructure.
• Deploys are all-or-nothing. Content is verified in S3 before CloudFront
  ever points at it, and routing code is content-addressed so a new rewrite is
  never served against an old release.
• Verification is byte-exact. Comparisons use SHA-256 of the actual HTTPS
  response body, not ETags or timestamps.
Exit codes
• 0 — success
• 1 — configuration, verification, or AWS operation failed
• 2 — waiting for ACM DNS validation (add the printed CNAME and rerun)
Development
sh
make build              # ./bin/xiaoyu
make release VERSION=v1.2.3
make test               # never contacts AWS
make vet fmt
make release requires a strict SemVer 2.0 version with a leading v and
checks the finished binary.
Real-AWS integration tests
These are behind a build tag and an explicit opt-in because they create
billable resources. Use a dedicated test domain and account.
sh
export AWS_ACCESS_KEY_ID='...'
export AWS_SECRET_ACCESS_KEY='...'
export AWS_REGION='eu-west-1'
export XIAOYU_INTEGRATION_DOMAIN='xiaoyu-test.example.com'
export XIAOYU_RUN_AWS_INTEGRATION=1

make test-integration
make test-integration-compile   # compile only, no AWS
The first run may stop for ACM validation; add the printed CNAME and rerun,
then add the site DNS record. Use
XIAOYU_INTEGRATION_SKIP_CUSTOM_DOMAIN=1 only while that record is pending.
The suite covers provisioning idempotency, anonymous S3 access returning
403, HTTP→HTTPS redirects, byte-exact content over the custom domain, sync
and single-file invalidation, atomic releases and rollback, and resource
reuse. Resources are deliberately left in place; run xiaoyu destroy --yes
when finished.
Additional gated suites:
• XIAOYU_RUN_RETAIN_RECOVERY_INTEGRATION=1 — destructive
  destroy --retain-bucket and recovery round-trip.
• XIAOYU_RUN_IAM_INTEGRATION=1 — creates and cleans up a dedicated IAM user
  (the test domain's distribution must already exist).
• XIAOYU_RUN_IAM_DEPLOY_INTEGRATION=1 — additionally provisions and proves
  the generated user can deploy.
XIAOYU_INTEGRATION_BUCKET overrides the test bucket,
XIAOYU_INTEGRATION_TIMEOUT (default 30m) sets the lifecycle timeout, and
XIAOYU_INTEGRATION_TEST_TIMEOUT (default 35m) sets Make's outer Go test
timeout, which must exceed the lifecycle timeout by more than two minutes.
The project website
`website/` holds the xiaoyu.sh site: plain HTML/CSS, system
monospace fonts, no dependencies or external assets. The online manual is
generated from this README, so documentation stays in sync.
sh
make website                      # downloads + pages, into dist/website/
make website VERSION=v1.2.3       # stamp a real version into the binaries
make website-preview              # http://127.0.0.1:8080
make website runs make downloads first, which cross-compiles the binary for
macOS and Linux into dist/website/download/, packages the tracked source with
git archive, and writes SHA256SUMS.txt. The page build refuses to run if
those artifacts are missing, so its download links can never 404. Artifacts are
deliberately named xiaoyu-<os>-<arch>.tar.gz: a file named exactly xiaoyu
is always excluded from uploads and could never be published. The version in
dist/website-version.txt is shown on the page; it is dev unless you pass
VERSION.
#### Publishing the CloudFormation template
The build copies `cloudformation/bootstrap.yaml`
to dist/website/aws/bootstrap.yaml as the readable copy. The launch button
cannot point there: CloudFormation only accepts a TemplateURL served from an
S3 bucket, and its docs state that "URLs from S3 static websites are not
supported". So the machine-readable copy needs a small public bucket of its
own. The published copy lives at:
https://xiaoyu-bootstrap.s3.ap-northeast-2.amazonaws.com/bootstrap.yaml
That is the default the page is built with; set XIAOYU_TEMPLATE_URL at build
time to point somewhere else. The launch link creates the stack in the region
parsed from that URL — ap-northeast-2 here — because CloudFormation reads the
template from S3 and cross-region reads are not dependably supported. The stack
holds only a global IAM user, so its region says nothing about where the site
lives; override it with XIAOYU_STACK_REGION if you move the template.
The key is not versioned, so re-uploading changes what every existing launch
link creates. Edit the template in place only for fixes you want everyone to
get; publish a new key for anything else.
dist/website/ is a self-contained site root: upload it to any static host, or
publish it with xiaoyu from inside the folder. The build writes a
.xiaoyu.yaml there (copied from `website.xiaoyu.yaml`
with source: .), and that filename is always excluded from uploads, so it is
never published.
sh
make build website
cd dist/website
xiaoyu config check
xiaoyu bootstrap-user --deploy   # first time, with temporary admin credentials
xiaoyu deploy                    # every time after that
Edit website.xiaoyu.yaml in the repository root to change site settings; the
copy in dist/website/ is regenerated by every build.
Then follow the quick start from step 4. xiaoyu.sh is an
apex domain, so use a DNS provider with ALIAS/ANAME or CNAME flattening.
Building or previewing the website never touches AWS or DNS.
Migrating from s3site
This tool used to be called s3site. Existing .s3site.yaml files and
S3SITE_ environment variables still work. See
MIGRATING-FROM-S3SITE.md.