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
42 changes: 38 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,38 @@ jobs:
- name: Previews reuse the one site definition
run: grep -q 'import site' deploy/preview-up.sh

# #113: a preview runs on its own copy of dev, migrated by the pull
# request's own image. Each line has a silent failure: without the copy
# the preview is back on dev's database and every migration-carrying
# pull request previews as a server error; without the migrate step the
# copy carries main's schema, which is the same failure with more steps;
# without DATABASE_URL on BOTH containers — the migrator and the app —
# one of them keeps the instance .env's value and never sees the copy.
#
# Matched on the psql flag rather than the SQL alone: both files also
# MENTION these statements, in a comment and in a by-hand instruction,
# and a check a comment can satisfy is not a check.
- name: A preview gets its own database, migrated, and is pointed at it
run: |
grep -q -- '-c "create database' deploy/preview-up.sh
grep -q -- '/app/migrate.mjs' deploy/preview-up.sh
test "$(grep -c -- '--env DATABASE_URL' deploy/preview-up.sh)" -ge 2
# And it goes away again, both ways: when the pull request closes, and —
# because an older branch's preview-up.sh knows nothing about copies —
# whenever a copy is found with no container to own it. A copy of every
# dev account's rows, left on a disk that has already filled once
# (#119), is the cost of getting this wrong in the other direction.
- name: Closing a preview drops its database
run: grep -q -- '-c "drop database if exists' deploy/preview-down.sh
- name: A copy with no container is swept up
run: grep -q 'no \$container container to own it' deploy/preview-up.sh
# The nightly copy of the databases (#168) must not copy the copies: a
# preview clone is dev's data again, and a night with two previews open
# would be three times the size of one without — which the shrink guard
# reads as a copy gone bad, and then refuses every night after it.
- name: The nightly copy skips the preview copies
run: grep -q "not like 'platform..pr..%'" deploy/backup-db.sh

# Smoke on every push and PR (SPEC §6): the database-less project only. No
# DATABASE_URL_TEST here on purpose — that is what makes this job prove the
# fail-closed behaviour (401s, redirects to /login, 404s) with no database at
Expand Down Expand Up @@ -291,8 +323,9 @@ jobs:
# The log is the server's whole stdout, and the dev e-mail transport
# prints every message body — verification and reset links included. The
# database behind those links is a throwaway container today, but the
# artifact outlives the job by a week and SPEC §8 puts PR previews on the
# shared dev database, so the links never leave the runner intact.
# artifact outlives the job by a week and the same accounts exist on dev
# and in every preview's copy of it (#113), so the links never leave the
# runner intact.
- name: Redact action links from the server log
if: failure()
run: |
Expand Down Expand Up @@ -381,8 +414,9 @@ jobs:
# The log is the server's whole stdout, and the dev e-mail transport
# prints every message body — verification and reset links included. The
# database behind those links is a throwaway container today, but the
# artifact outlives the job by a week and SPEC §8 puts PR previews on the
# shared dev database, so the links never leave the runner intact.
# artifact outlives the job by a week and the same accounts exist on dev
# and in every preview's copy of it (#113), so the links never leave the
# runner intact.
- name: Redact action links from the server log
if: failure()
run: |
Expand Down
23 changes: 22 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ export function ownerKey(
| Environment | Where | Database | Deployment |
| ----------- | ------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Local | developer machine | remote `waw` over an SSH tunnel (per developer: `platform_<github-handle>`, for Dawid `platform_devski`) | — |
| PR preview | dev instance, `*.dev.architektow3d.pl` | shared dev | automatic on PR open, deleted after merge; `*.dev.architektow3d.pl` (#31) |
| PR preview | dev instance, `*.dev.architektow3d.pl` | a copy of dev, taken when the preview starts and dropped with it (#113) | automatic on PR open, deleted after merge; `*.dev.architektow3d.pl` (#31) |
| Dev | OVH `waw`, d2-2 (€7), `dev.architektow3d.pl` | Postgres in a container | automatic from `main` |
| Prod | OVH `eu-west-par`, b3-8 (€35), `architektow3d.pl` | Managed PostgreSQL (€59) | **manual**: an approval gate in the deploy workflow, or a `vX.Y.Z` tag |

Expand Down Expand Up @@ -337,6 +337,27 @@ export function ownerKey(
plain Docker image). The one line worth paying from day one is the managed database, and
not for performance: prod holds real accounts and photos, so someone else's backups and
patching is the product being bought. To settle with #24.
- **A preview runs on a copy of dev, not on dev** (#113, decided 09.09.2026): when a preview
starts, `deploy/preview-up.sh` copies dev's database into `platform_pr_<n>` with `pg_dump`,
runs the pull request's own migrator against the copy, and points the container at it;
closing the pull request drops it. Previews never migrated, so before this every pull
request that added a column its pages read previewed as a server error — twice in five days
(#112, #170), each time exactly when the preview existed to be looked at. Not "the preview
migrates the shared database": a pull request revised after its preview ran would leave a
migration in dev's journal whose hash no longer matched the file, and the next deployment of
`main` would fail on it. What a preview therefore is: dev's data as of its start, plus this
pull request's schema. Accounts created in a preview, and the rows recording what was
uploaded there, die with it. Three things follow from the copy and are enforced rather
than hoped for: dev's `sessions` and `verifications` are emptied in the copy (signing out
on dev cannot reach a copy, so a copied session would be a credential nothing could
revoke — you sign in to a preview); a delete may only touch keys under the environment's
own prefix (`src/lib/storage.ts`), because the copy's rows name dev's objects in the
bucket everything shares; and a copy with no container is dropped by the next
`preview-up.sh`, since only closing the pull request would otherwise remove one.
What stays shared: the bucket (new writes under `pr-<n>/`), the signing key, the instance.
**The signing key being shared is the thing production must not inherit** — one
`AUTH_SECRET` across environments plus a copy of the rows is how a session from one
becomes a session in another; #24 gives production its own.
- **Every wait on the database has a deadline, and each environment sets its own** (#172):
the pool answers a caller it cannot give a connection to within five seconds instead of
queueing them for ever, and the server cuts off a statement that runs past ten seconds or
Expand Down
7 changes: 6 additions & 1 deletion deploy/backup-db.sh
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,13 @@ roles=$(psql --command "select rolname from pg_roles where rolname !~ '^pg_' and

# The test databases are excluded: a test run recreates them from nothing, and
# copying them would double the bytes for something nobody would ever restore.
# So are the preview copies (#113): `platform_pr_<n>` IS a copy of dev, made
# an hour ago and dropped when the pull request closes. Copying those would
# put dev's data in the object two or three times over — and, worse, would
# make the size of a night's copy depend on how many pull requests happened to
# be open, which the shrink guard below reads as a copy going bad.
# `template1` and `postgres` hold nothing of ours.
databases=$(psql --command "select datname from pg_database where datallowconn and datname like 'platform\_%' and datname not like '%\_test\_%' order by 1")
databases=$(psql --command "select datname from pg_database where datallowconn and datname like 'platform\_%' and datname not like '%\_test\_%' and datname not like 'platform\_pr\_%' order by 1")
if [ -z "$databases" ]; then
echo "no platform_* database to copy — is this the right cluster?" >&2
exit 1
Expand Down
33 changes: 29 additions & 4 deletions deploy/preview-down.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# Runs ON the instance. Removes the preview of one pull request (#31): the
# container, its route, and nothing else. Safe to run twice, and safe to run
# for a preview that was never started — closing a pull request whose preview
# failed to come up must still leave the instance clean.
# container, its route, its database, and nothing else. Safe to run twice, and
# safe to run for a preview that was never started — closing a pull request
# whose preview failed to come up must still leave the instance clean.
#
# Expects in the environment: PR (the number).
set -euo pipefail
Expand All @@ -15,6 +15,7 @@ esac

NAME="pr-$PR"
ROUTE="/opt/platform-lite/previews/$NAME.caddy"
CLONE="platform_pr_$PR"

if docker rm --force "$NAME" >/dev/null 2>&1; then
echo "container $NAME removed"
Expand All @@ -32,11 +33,35 @@ else
echo "no route file to remove"
fi

# The preview's own copy of dev (#113). After the container, so nothing is
# still connected — and WITH (FORCE) for the case where something is anyway
# (a one-off `docker run`, a psql session left open). A copy left behind is
# not free: it is dev's data again on a disk that has already filled once
# (#119), and the next preview of this pull request would drop it unread.
#
# Not fatal if it fails: the container and the route are already gone, and
# closing a pull request must not end in a red job over a database nobody is
# using. It says so loudly instead.
if docker exec -i postgres psql -U postgres -v ON_ERROR_STOP=1 -q -d postgres \
-c "drop database if exists \"$CLONE\" with (force)" >/dev/null 2>&1; then
echo "database $CLONE dropped"
else
echo "WARNING: database $CLONE could not be dropped — drop it by hand:" >&2
echo " docker exec postgres psql -U postgres -c 'drop database if exists \"$CLONE\" with (force)'" >&2
fi

# What is deliberately NOT removed: objects under the `pr-<n>/` prefix in the
# bucket. Deleting them needs the S3 credentials, which this script has no
# reason to hold, and they cost fractions of a cent — the bucket's lifecycle
# rule already expires staged uploads. Sweeping preview prefixes belongs with
# the account-deletion work, which needs the same object-removal path.
# the account-deletion work, which needs the same object-removal path. Since
# #113 the rows naming those objects go with the database, so what is left is
# unreferenced by anything.

echo "remaining previews:"
docker ps --filter 'name=^pr-[0-9]+$' --format ' {{.Names}} {{.Status}}' || true

echo "remaining preview databases:"
docker exec -i postgres psql -U postgres -At -d postgres \
-c "select datname from pg_database where datname like 'platform\_pr\_%' order by datname" ||
echo " (could not ask PostgreSQL)"
Loading
Loading