Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/preview-policy-fails-closed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hephaestus": patch
---

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.
17 changes: 12 additions & 5 deletions docker/preview/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,16 @@ already seeded PR preserves changes made while testing. A new preview volume get

## Preview policy

`seed-loader` applies [`sanitize.sql`](./sanitize.sql) to the restored clone before the application
server starts. A clone is another instance's live database, so the policy answers two questions, and
the file is organised by them.
`seed-loader` applies a policy to the restored clone before the application server starts, then
verifies it took effect and only then writes the seed marker. A clone is another instance's live
database, so the policy answers two questions, and it is organised by them.

The SQL is inline in `compose.app.yaml` rather than a mounted `.sql` file. Coolify materialises a
relative bind mount as an empty managed *directory*, and `psql < <directory>` reads nothing and exits
0 — which once let a preview start on a clone that had kept every trigger, binding and credential of
the instance it came from, behind a seed marker claiming otherwise. The marker is now written only
against the database's own answer: any live trigger, binding, job, or inherited identity fails the
deployment.

### Silence — a clone must not act

Expand Down Expand Up @@ -108,8 +115,8 @@ effective value and is not.

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

Expand Down
75 changes: 69 additions & 6 deletions docker/preview/compose.app.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,18 +115,81 @@ services:
-U hephaestus -d hephaestus \
--exit-on-error --no-owner --no-privileges < /tmp/seed.dump

# The policy is inline rather than a mounted .sql file: Coolify materialises a relative bind
# mount as an empty managed directory, and `psql < <directory>` reads nothing and exits 0 —
# a clone that silently kept every trigger, binding and credential of the instance it came
# from. The verification below is what actually holds the line; see README.md.
echo "Applying the preview policy"
test -s /preview/sanitize.sql
docker exec -i "$$PREVIEW_PG" psql \
-v ON_ERROR_STOP=1 -U hephaestus -d hephaestus < /preview/sanitize.sql
docker exec -i "$$PREVIEW_PG" psql -v ON_ERROR_STOP=1 -U hephaestus -d hephaestus <<'SQL'
-- 1. Silence: a clone must not act.
UPDATE workspace
SET practice_review_auto_trigger_enabled = FALSE,
practice_review_manual_trigger_enabled = FALSE;

UPDATE workspace_agent_binding
SET enabled = FALSE,
updated_at = NOW()
WHERE purpose = 'PRACTICE_REVIEW';

UPDATE review_sweep_schedule
SET enabled = FALSE,
updated_at = NOW();

UPDATE agent_job
SET status = 'CANCELLED',
completed_at = COALESCE(completed_at, NOW()),
worker_id = NULL,
error_message = 'Cancelled when staging data was cloned into a preview.',
cancellation_reason = NULL
WHERE status IN ('QUEUED', 'RUNNING');

UPDATE agent_job
SET delivery_status = 'FAILED',
error_message = CONCAT_WS(E'\n', NULLIF(error_message, ''),
'Delivery paused when staging data was cloned into a preview.')
WHERE status = 'COMPLETED'
AND delivery_status = 'PENDING';

UPDATE sync_job
SET status = 'CANCELLED',
cancel_requested = TRUE,
finished_at = COALESCE(finished_at, NOW()),
heartbeat_at = NULL,
error_summary = 'Cancelled when staging data was cloned into a preview.'
WHERE status IN ('PENDING', 'RUNNING');

-- 2. Re-home: a clone must not keep the source instance's identity. Each table is rebuilt
-- from this deployment's own configuration on boot. identity_link keys on identity_provider,
-- not on login_provider, so accounts survive.
DELETE FROM issued_jwt;
DELETE FROM jwt_signing_key;
DELETE FROM login_provider;
SQL

# The marker is a promise that the policy is in force, so it is written only against the
# database's own answer. Anything that leaves a live trigger, binding, job or inherited
# identity behind fails the deployment instead of starting an application server on a clone
# that can act as the instance it was copied from.
echo "Verifying the preview policy took effect"
LIVE=$$(docker exec "$$PREVIEW_PG" psql -tAX -v ON_ERROR_STOP=1 -U hephaestus -d hephaestus -c "
SELECT (SELECT count(*) FROM workspace
WHERE practice_review_auto_trigger_enabled OR practice_review_manual_trigger_enabled)
+ (SELECT count(*) FROM workspace_agent_binding WHERE enabled AND purpose = 'PRACTICE_REVIEW')
+ (SELECT count(*) FROM review_sweep_schedule WHERE enabled)
+ (SELECT count(*) FROM agent_job WHERE status IN ('QUEUED', 'RUNNING'))
+ (SELECT count(*) FROM sync_job WHERE status IN ('PENDING', 'RUNNING'))
+ (SELECT count(*) FROM login_provider)
+ (SELECT count(*) FROM jwt_signing_key)
+ (SELECT count(*) FROM issued_jwt)")
if [ "$$LIVE" != "0" ]; then
echo "Preview policy did not take effect ($$LIVE rows still live); refusing to mark this preview seeded"
exit 1
fi

docker exec "$$PREVIEW_PG" touch /var/lib/postgresql/data/.hephaestus-preview-seeded
echo "Seed loaded; reviews are paused and the preview owns its login apps and signing key"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
# The policy this preview is held to, kept next to the stack it applies to rather than inline
# in a shell heredoc where it cannot be read, reviewed or diffed as SQL.
- ./sanitize.sql:/preview/sanitize.sql:ro
restart: "no"
deploy:
resources:
Expand Down
72 changes: 0 additions & 72 deletions docker/preview/sanitize.sql

This file was deleted.

Loading