Skip to content

Commit 4c77859

Browse files
feat(ci): seed previews from staging and read its event stream
Restores what #1455 built and #1557 dropped: a preview starts from a pg_dump of staging's database and consumes staging's JetStream, so it is worth looking at rather than an empty install. The seed loader runs before the application server may boot. It cancels queued work, disables every review trigger, and drops the instance identity, then verifies that against the database and refuses to mark the preview seeded if the policy did not take — a preview that cannot be silenced stays down. It holds the Docker socket read-only because pg_dump and psql run inside the two database containers; check-preview-stack.ts now refuses that mount on any other service, and refuses it writable on this one. The local broker is gone. The application server joins staging's shared-network for its broker, with a durable named per deploy so previews never compete for one consumer, and a 72h inactivity window because a preview is deleted rather than shut down. staging-shared is external and named, which the sandbox check now distinguishes from the project-scoped networks every preview would share. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017VKWqbmrPJFv8aKZBp36uD
1 parent 9bdcf81 commit 4c77859

8 files changed

Lines changed: 301 additions & 68 deletions

File tree

.changeset/previews-deploy-on-purpose.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@
44

55
Pull request previews are now self-service. Add the `preview` label to a pull request in this repository and it deploys; every commit after that redeploys on its own. A preview waits only for its images to be published, never for the test suite, so it exists even when the tests are red — and it runs the same artifacts staging and production run, so what you see is what ships. A comment on the pull request carries the preview link, and GitHub's native deployment link opens it too. Removing the label, closing the pull request, or converting it back to draft removes the stack. Up to three previews run at once by default, and when the host is full the pull request comment names the ones holding the slots.
66

7-
Preview stacks use their own database, message broker, credentials, and Docker networks, and reach neither the staging Docker socket, its data, nor any integration credential. Previews never run for forks, nor for changes to the deployment workflows themselves. Stacked pull requests each get their own preview.
7+
Each preview starts from a copy of staging's database, so workspaces and synced work are already there — and that copy is silenced before the app starts: review triggers, agent bindings and sweep schedules off, queued jobs cancelled, and the sign-in identity dropped so the preview issues its own tokens. It reads staging's event stream on a consumer of its own. Agent runs and inbound webhooks stay off. Previews never run for forks, nor for changes to the deployment workflows themselves. Stacked pull requests each get their own preview.
88

9-
**Operators:** follow the preview runbook before enabling the Coolify application. It requires a `preview` repository label, a preview-only Coolify application, two scoped Coolify secrets, and the repository variables listed there — including the optional `PREVIEW_MAX_ACTIVE` limit. Keep Coolify's automatic repository webhook disabled.
9+
**Operators:** follow the preview runbook before enabling the Coolify application. It requires a `preview` repository label, a preview-only Coolify application, two scoped Coolify secrets, and the repository variables listed there — including the optional `PREVIEW_MAX_ACTIVE` limit. Previews must be deployed onto the staging host: they seed from its database and read its event stream. Keep Coolify's automatic repository webhook disabled.

docker/preview/.env.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ HEPHAESTUS_AUTH_BOOTSTRAP_ADMINS=
2626
GH_OAUTH_CLIENT_ID=
2727
GH_OAUTH_CLIENT_SECRET=
2828

29+
# Staging's broker, reached over its external `shared-network`. The application server consumes from
30+
# it with a durable named per deploy, so previews never compete for one consumer.
31+
NATS_SERVER=nats://nats-server:4222
32+
33+
# The running staging PostgreSQL container. seed-loader takes one pg_dump from it on a preview's
34+
# first deploy, applies the preview policy, and refuses to mark the preview seeded if that policy
35+
# did not take — so a failure here leaves the preview un-booted rather than live on unsilenced data.
36+
PREVIEW_SEED_SOURCE_CONTAINER=app-postgres-1
37+
PREVIEW_SEED_SOURCE_USERNAME=root
38+
PREVIEW_SEED_SOURCE_DATABASE=hephaestus
39+
2940
# Coolify supplies these values for each PR. Do not define them manually.
3041
# SOURCE_COMMIT=
3142
# COOLIFY_BRANCH=

docker/preview/README.md

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,31 @@ hold, each enforced before Coolify is asked to deploy:
2525
attestation naming this repository's `reusable-docker-build.yml` as signer, checked before Coolify
2626
is asked to pull it. Nothing is built on the deployment host, so a preview cannot pass on a
2727
lookalike of the shipped image.
28-
- The stack has a fresh PostgreSQL database and private NATS server. It has no staging network,
29-
staging database copy, staging encryption key, integration credentials, or Docker socket.
30-
- Agent execution, repository checkout, inbound webhooks, recurring sync, and external integrations
31-
are disabled. The stack has normal outbound internet access, but no staging credential, Docker
32-
network membership, or host control socket.
33-
34-
The application server runs without Linux capabilities and with `no-new-privileges`. PostgreSQL and
35-
NATS data live in PR-specific volumes. The stack declares no networks of its own: Coolify runs every
36-
preview of this application under one Compose project named after the application UUID, so a network
37-
named here would be shared by all of them at once. Each preview gets Coolify's own per-preview
38-
network instead, which its proxy joins to reach the webapp and API.
28+
- The database is this preview's own, seeded from staging so it starts with real data. The clone is
29+
taken with `pg_dump` — staging's data volume is never mounted — and the preview policy runs before
30+
the application server is allowed to boot: every review trigger, agent binding and sweep schedule
31+
is disabled, queued jobs are cancelled, pending deliveries are failed, and the instance identity
32+
(`login_provider`, `jwt_signing_key`, `issued_jwt`) is dropped so the preview signs its own tokens.
33+
The seed loader verifies that against the database and refuses to mark the preview seeded
34+
otherwise, so a policy that silently did not apply leaves the preview un-booted rather than live.
35+
- The application server reads staging's JetStream, so a preview sees the events a shared GitHub App
36+
delivers there. Its durable is named per deploy, so previews never compete for one consumer, and
37+
it expires 72h after the preview stops reading — a preview is deleted, not shut down, so it never
38+
removes its own.
39+
- Agent execution, repository checkout, inbound webhooks and recurring sync stay disabled, which is
40+
what keeps a stack holding real data from acting on it.
41+
42+
Two things follow from that, and both are deliberate rather than incidental. Only the seed loader
43+
holds the Docker socket, read-only, because `pg_dump` and `psql` run inside the two database
44+
containers; every service that runs pull-request code is refused it. And only the application server
45+
joins staging's network, to reach the broker. The database and the SPA stay on Coolify's per-preview
46+
network — the one it creates and attaches its proxy to. This file defines no network of its own:
47+
Coolify runs every preview of this application under one Compose project named after the application
48+
UUID, so a network defined here would be `<uuid>_backend` for all of them at once, private-looking
49+
and shared. `staging-shared` is joined by name, which makes it a decision rather than a side effect.
50+
51+
The application server runs without Linux capabilities and with `no-new-privileges`. PostgreSQL
52+
keeps a PR-specific volume.
3953

4054
Deployment authority is deliberately the same as push authority;
4155
[ADR 0035](../../docs/decisions/0035-pull-request-previews-are-label-gated.md) records why, and what
@@ -49,16 +63,16 @@ pull request's own copy of it defines that pull request's stack. Two rules hold
4963
works alone: the controller refuses to deploy a head that introduces changes anywhere under
5064
`docker/preview/` — compared against the default branch, so a stacked layer cannot inherit an edit
5165
from the layer below — and `ci-compose-validate.yml` renders the file on every pull request and fails if
52-
the stack gains a way out of its sandbox — a socket, a build stage, a published port, an external
53-
network, an unbounded memory limit, a network every preview would share, or a flipped integration
54-
switch.
66+
the stack gains a way out of its sandbox — a build stage, a published port, an unbounded memory
67+
limit, a network every preview would share, the Docker socket on anything but the seed loader or
68+
writable on that, or a flipped integration switch.
5569
`scripts/check-preview-stack.ts` is the authoritative list; this paragraph is not.
5670

5771
## Host capacity
5872

59-
Each stack is capped at about 3 GiB (2 GiB application server, 512 MiB PostgreSQL, 256 MiB each for
60-
NATS and the webapp). Its CPU ceilings total 2.0 across the four services, so several previews
61-
oversubscribe a small host on paper. That is intended: a preview is idle almost all the time, and a
73+
Each stack is capped at about 2.8 GiB once running (2 GiB application server, 512 MiB PostgreSQL,
74+
256 MiB webapp); the seed loader adds 512 MiB while it runs and then exits. Its CPU ceilings total
75+
2.75 across the four services, so several previews oversubscribe a small host on paper. That is intended: a preview is idle almost all the time, and a
6276
ceiling stops one stack from taking the box rather than reserving capacity for it.
6377

6478
`PREVIEW_MAX_ACTIVE` caps concurrent previews and defaults to 3. It has to leave roughly 3 GiB of
@@ -89,7 +103,12 @@ Create one Docker Compose application for `ls1intum/Hephaestus` on branch `main`
89103
5. Leave `SOURCE_COMMIT`, `COOLIFY_BRANCH`, and the `SERVICE_*` variables to Coolify. Define nothing
90104
on this application beyond `.env.example`; anything else does not belong here.
91105
6. Disable **Connect to predefined network**. Coolify attaches its proxy to each generated preview
92-
network; the services do not need the shared `coolify` network.
106+
network; the services do not need the shared `coolify` network. The one network this stack joins,
107+
staging's `shared-network`, it names itself.
108+
7. The deployment host must be the one running staging, because the seed loader reaches staging's
109+
Postgres through the host's Docker socket and the application server reaches its broker over
110+
`shared-network`. Both are checked at deploy time: a missing seed source fails the preview
111+
rather than starting it on an empty database.
93112

94113
## GitHub configuration
95114

0 commit comments

Comments
 (0)