Skip to content

Commit 7431d04

Browse files
fix(docker): make the preview policy fail closed
Coolify materialises a relative bind mount as an empty managed directory, so the mounted sanitize.sql was a directory inside the container. `test -s` passes on a directory and `psql < <directory>` reads nothing and exits 0, so the seeder wrote its success marker having applied nothing: a preview came up on a clone that still had every workspace trigger, the practice review binding, the source instance's OAuth apps and its signing key. The policy moves back inline, which is the form this Coolify version runs, and the marker is now written only against the database's own answer. Any live trigger, binding, job or inherited identity fails the deployment instead of starting an application server on a clone that can act as the instance it was copied from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EZADQeSx6zQNNqsdu7AAqZ
1 parent 506b379 commit 7431d04

4 files changed

Lines changed: 86 additions & 83 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"hephaestus": patch
3+
---
4+
5+
A pull request preview now refuses to start unless the safeguards it promises are actually in place. Previews begin from a copy of a live database, and the step that pauses reviews and replaces the copied sign-in configuration could fail without saying so, leaving a preview running against real data with its review triggers still enabled. That step is now checked against the database itself, and a preview that does not pass it fails its deployment instead of coming up.

docker/preview/README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,16 @@ already seeded PR preserves changes made while testing. A new preview volume get
2424

2525
## Preview policy
2626

27-
`seed-loader` applies [`sanitize.sql`](./sanitize.sql) to the restored clone before the application
28-
server starts. A clone is another instance's live database, so the policy answers two questions, and
29-
the file is organised by them.
27+
`seed-loader` applies a policy to the restored clone before the application server starts, then
28+
verifies it took effect and only then writes the seed marker. A clone is another instance's live
29+
database, so the policy answers two questions, and it is organised by them.
30+
31+
The SQL is inline in `compose.app.yaml` rather than a mounted `.sql` file. Coolify materialises a
32+
relative bind mount as an empty managed *directory*, and `psql < <directory>` reads nothing and exits
33+
0 — which once let a preview start on a clone that had kept every trigger, binding and credential of
34+
the instance it came from, behind a seed marker claiming otherwise. The marker is now written only
35+
against the database's own answer: any live trigger, binding, job, or inherited identity fails the
36+
deployment.
3037

3138
### Silence — a clone must not act
3239

@@ -108,8 +115,8 @@ effective value and is not.
108115

109116
Coolify parses the Compose file from the branch configured on the application — `main` — and stores it
110117
on the application record. A preview deployment builds the pull request's images but runs that stored
111-
definition, so a change to `compose.app.yaml` or [`sanitize.sql`](./sanitize.sql) is **not** exercised
112-
by the preview of the pull request that makes it. Reviewing a change here means reading it; the first
118+
definition, so a change to `compose.app.yaml` is **not** exercised by the preview of the pull request
119+
that makes it. Reviewing a change here means reading it; the first
113120
preview that actually runs it is the next one created after the change is merged and Coolify has
114121
re-read `main`.
115122

docker/preview/compose.app.yaml

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,18 +115,81 @@ services:
115115
-U hephaestus -d hephaestus \
116116
--exit-on-error --no-owner --no-privileges < /tmp/seed.dump
117117
118+
# The policy is inline rather than a mounted .sql file: Coolify materialises a relative bind
119+
# mount as an empty managed directory, and `psql < <directory>` reads nothing and exits 0 —
120+
# a clone that silently kept every trigger, binding and credential of the instance it came
121+
# from. The verification below is what actually holds the line; see README.md.
118122
echo "Applying the preview policy"
119-
test -s /preview/sanitize.sql
120-
docker exec -i "$$PREVIEW_PG" psql \
121-
-v ON_ERROR_STOP=1 -U hephaestus -d hephaestus < /preview/sanitize.sql
123+
docker exec -i "$$PREVIEW_PG" psql -v ON_ERROR_STOP=1 -U hephaestus -d hephaestus <<'SQL'
124+
-- 1. Silence: a clone must not act.
125+
UPDATE workspace
126+
SET practice_review_auto_trigger_enabled = FALSE,
127+
practice_review_manual_trigger_enabled = FALSE;
128+
129+
UPDATE workspace_agent_binding
130+
SET enabled = FALSE,
131+
updated_at = NOW()
132+
WHERE purpose = 'PRACTICE_REVIEW';
133+
134+
UPDATE review_sweep_schedule
135+
SET enabled = FALSE,
136+
updated_at = NOW();
137+
138+
UPDATE agent_job
139+
SET status = 'CANCELLED',
140+
completed_at = COALESCE(completed_at, NOW()),
141+
worker_id = NULL,
142+
error_message = 'Cancelled when staging data was cloned into a preview.',
143+
cancellation_reason = NULL
144+
WHERE status IN ('QUEUED', 'RUNNING');
145+
146+
UPDATE agent_job
147+
SET delivery_status = 'FAILED',
148+
error_message = CONCAT_WS(E'\n', NULLIF(error_message, ''),
149+
'Delivery paused when staging data was cloned into a preview.')
150+
WHERE status = 'COMPLETED'
151+
AND delivery_status = 'PENDING';
152+
153+
UPDATE sync_job
154+
SET status = 'CANCELLED',
155+
cancel_requested = TRUE,
156+
finished_at = COALESCE(finished_at, NOW()),
157+
heartbeat_at = NULL,
158+
error_summary = 'Cancelled when staging data was cloned into a preview.'
159+
WHERE status IN ('PENDING', 'RUNNING');
160+
161+
-- 2. Re-home: a clone must not keep the source instance's identity. Each table is rebuilt
162+
-- from this deployment's own configuration on boot. identity_link keys on identity_provider,
163+
-- not on login_provider, so accounts survive.
164+
DELETE FROM issued_jwt;
165+
DELETE FROM jwt_signing_key;
166+
DELETE FROM login_provider;
167+
SQL
168+
169+
# The marker is a promise that the policy is in force, so it is written only against the
170+
# database's own answer. Anything that leaves a live trigger, binding, job or inherited
171+
# identity behind fails the deployment instead of starting an application server on a clone
172+
# that can act as the instance it was copied from.
173+
echo "Verifying the preview policy took effect"
174+
LIVE=$$(docker exec "$$PREVIEW_PG" psql -tAX -v ON_ERROR_STOP=1 -U hephaestus -d hephaestus -c "
175+
SELECT (SELECT count(*) FROM workspace
176+
WHERE practice_review_auto_trigger_enabled OR practice_review_manual_trigger_enabled)
177+
+ (SELECT count(*) FROM workspace_agent_binding WHERE enabled AND purpose = 'PRACTICE_REVIEW')
178+
+ (SELECT count(*) FROM review_sweep_schedule WHERE enabled)
179+
+ (SELECT count(*) FROM agent_job WHERE status IN ('QUEUED', 'RUNNING'))
180+
+ (SELECT count(*) FROM sync_job WHERE status IN ('PENDING', 'RUNNING'))
181+
+ (SELECT count(*) FROM login_provider)
182+
+ (SELECT count(*) FROM jwt_signing_key)
183+
+ (SELECT count(*) FROM issued_jwt)")
184+
if [ "$$LIVE" != "0" ]; then
185+
echo "Preview policy did not take effect ($$LIVE rows still live); refusing to mark this preview seeded"
186+
exit 1
187+
fi
122188
123189
docker exec "$$PREVIEW_PG" touch /var/lib/postgresql/data/.hephaestus-preview-seeded
124190
echo "Seed loaded; reviews are paused and the preview owns its login apps and signing key"
125191
volumes:
126192
- /var/run/docker.sock:/var/run/docker.sock:ro
127-
# The policy this preview is held to, kept next to the stack it applies to rather than inline
128-
# in a shell heredoc where it cannot be read, reviewed or diffed as SQL.
129-
- ./sanitize.sql:/preview/sanitize.sql:ro
130193
restart: "no"
131194
deploy:
132195
resources:

docker/preview/sanitize.sql

Lines changed: 0 additions & 72 deletions
This file was deleted.

0 commit comments

Comments
 (0)