Add staging + PR-preview environments on the flagship
Implements the staging-environments OpenSpec change. Persistent staging at staging.trails.cool / planner.staging.trails.cool deploys from main; PR opens get a journal-only preview at pr-<N>.staging.trails.cool that shares the persistent planner. cd-staging.yml builds tagged images, manages per-PR Postgres databases and Caddyfile snippets, evicts the oldest preview at the cap of 3, and tears everything down on PR close. staging-cleanup.yml runs weekly to sweep orphaned previews. DNS records (staging + *.staging A/AAAA) already applied to production via tofu. Caddy approach: per-PR Caddyfile snippets imported from /etc/caddy/sites/ and reloaded on each PR event — no wildcard / on-demand TLS, no router service. Production compose gains a trails-shared network for the staging project to reach Postgres, and host.docker.internal on Caddy so it can reverse-proxy staging containers published on the host loopback. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
795ec2215c
commit
c8a7a0b253
11 changed files with 818 additions and 29 deletions
|
|
@ -69,3 +69,52 @@ planner.{$DOMAIN:trails.cool} {
|
|||
lb_try_interval 250ms
|
||||
}
|
||||
}
|
||||
|
||||
# ── Staging ──────────────────────────────────────────────────────────────
|
||||
# Persistent staging instance. CSP allow-lists hardcode `staging.trails.cool`
|
||||
# rather than `{$DOMAIN}` because the Caddy container runs with the
|
||||
# production DOMAIN env (`trails.cool`); staging blocks need their own domain
|
||||
# baked in. Upstreams are reached via `host.docker.internal` because the
|
||||
# staging compose project publishes its containers on the host's loopback
|
||||
# (127.0.0.1:3100/3101) rather than joining the production Caddy network.
|
||||
|
||||
staging.{$DOMAIN:trails.cool} {
|
||||
import security_headers
|
||||
import block_scanners
|
||||
log {
|
||||
output stdout
|
||||
format json
|
||||
}
|
||||
reverse_proxy host.docker.internal:3100 {
|
||||
lb_try_duration 30s
|
||||
lb_try_interval 250ms
|
||||
}
|
||||
}
|
||||
|
||||
planner.staging.{$DOMAIN:trails.cool} {
|
||||
header {
|
||||
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
||||
X-Content-Type-Options "nosniff"
|
||||
X-Frame-Options "DENY"
|
||||
Referrer-Policy "strict-origin-when-cross-origin"
|
||||
Permissions-Policy "camera=(), microphone=(), geolocation=()"
|
||||
# connect-src includes wss + https://*.staging so PR-preview journals
|
||||
# (pr-N.staging.trails.cool) can use this shared planner. PR previews
|
||||
# are journal-only; this is the planner they all talk to.
|
||||
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' blob:; worker-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.tile.openstreetmap.org; connect-src 'self' wss://*.staging.{$DOMAIN:trails.cool} https://*.staging.{$DOMAIN:trails.cool} https://staging.{$DOMAIN:trails.cool} https://*.sentry.io https://*.ingest.de.sentry.io; font-src 'self';"
|
||||
}
|
||||
import block_scanners
|
||||
log {
|
||||
output stdout
|
||||
format json
|
||||
}
|
||||
reverse_proxy host.docker.internal:3101 {
|
||||
lb_try_duration 30s
|
||||
lb_try_interval 250ms
|
||||
}
|
||||
}
|
||||
|
||||
# Per-PR preview snippets are written by cd-staging.yml into
|
||||
# /etc/caddy/sites/pr-<N>.caddyfile and picked up here on Caddy reload. The
|
||||
# glob is allowed to match nothing — Caddy treats an empty match as a no-op.
|
||||
import /etc/caddy/sites/*.caddyfile
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue