test: add PostgreSQL migration integration coverage
This commit is contained in:
@@ -47,3 +47,27 @@ jobs:
|
||||
|
||||
- name: Exercise PostgreSQL and S3 infrastructure
|
||||
run: bash scripts/smoke-infrastructure.sh
|
||||
|
||||
migration-smoke:
|
||||
needs: verify
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version-file: .node-version
|
||||
|
||||
- name: Activate pinned pnpm
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare [email protected] --activate
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Exercise transactional migrations against PostgreSQL
|
||||
run: pnpm test:migrations:integration
|
||||
|
||||
@@ -9,6 +9,15 @@ Before opening a change, run:
|
||||
pnpm verify
|
||||
```
|
||||
|
||||
Migration-runner changes also require the disposable live PostgreSQL suite:
|
||||
|
||||
```bash
|
||||
pnpm test:migrations:integration
|
||||
```
|
||||
|
||||
See the [testing guide](docs/development/testing.md) for the distinction between unit and live
|
||||
evidence and for credential-safe troubleshooting.
|
||||
|
||||
When adding or changing a module manifest, also run `pnpm modules:validate` directly for focused,
|
||||
path-aware discovery errors. It is included in `pnpm verify`.
|
||||
|
||||
|
||||
@@ -13,9 +13,10 @@ contracts, deterministic schema aggregation, and a cloneable backend-only module
|
||||
artifacts now have module-owned Drizzle contracts, deterministic offline discovery, explicit
|
||||
destructive-change declarations, and exact checksums. Core now provides read-only applied-history
|
||||
planning and a locked, whole-batch transactional PostgreSQL runner with exact external destructive
|
||||
approval matching. Live PostgreSQL runner integration coverage, production approval-provider
|
||||
composition, identity, HTTP routing, jobs, physical storage provisioning, surfaces, and feature
|
||||
modules remain later vertical slices.
|
||||
approval matching. A hermetic live PostgreSQL suite now proves history, rollback, statement
|
||||
breakpoints, advisory-lock concurrency and release, exact destructive approvals, and sanitization
|
||||
against the production adapter. Production approval-provider composition, identity, HTTP routing,
|
||||
jobs, physical storage provisioning, surfaces, and feature modules remain later vertical slices.
|
||||
|
||||
## Quick start
|
||||
|
||||
@@ -28,6 +29,7 @@ pnpm infrastructure:up
|
||||
pnpm modules:validate
|
||||
pnpm migrations:validate
|
||||
pnpm migrations:plan
|
||||
pnpm test:migrations:integration
|
||||
pnpm verify
|
||||
```
|
||||
|
||||
@@ -36,8 +38,9 @@ pnpm verify
|
||||
advisory lock and one transaction. Review the plan before applying. Destructive migrations are
|
||||
blocked by default and require an exact externally supplied approval; there is no force flag.
|
||||
|
||||
The runner currently has deterministic unit coverage through a database port. Live PostgreSQL
|
||||
migration-runner tests are deferred and are not yet claimed by `pnpm verify`.
|
||||
The runner has both deterministic unit coverage through a narrow database port and a separate live
|
||||
PostgreSQL integration suite. The live command creates and destroys disposable Docker
|
||||
infrastructure; it remains separate from the Docker-free `pnpm verify` gate.
|
||||
|
||||
Run `pnpm infrastructure:smoke` for an isolated end-to-end PostgreSQL and authenticated S3 check.
|
||||
|
||||
@@ -54,6 +57,7 @@ environment.
|
||||
- [Platform architecture](docs/architecture/platform-architecture.md)
|
||||
- [Repository boundaries](docs/architecture/repository-boundaries.md)
|
||||
- [Repository workflow](docs/development/repository-workflow.md)
|
||||
- [Testing](docs/development/testing.md)
|
||||
- [Module development](docs/module-development/README.md)
|
||||
- [Architecture decisions](docs/architecture/decisions/README.md)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -122,10 +122,14 @@ messages, machine-specific paths, causes, stacks, or partial execution results.
|
||||
are reported without hiding the primary failure and without copying underlying exception text.
|
||||
|
||||
The developer commands read `DATABASE_URL` from the environment. A local uncommitted `.env` may
|
||||
supply it, but commands never print it. This slice verifies database behavior through a narrow
|
||||
in-memory database port and unit tests. A real PostgreSQL integration suite for lock contention,
|
||||
DDL rollback, history creation, and driver behavior is deliberately deferred rather than claimed
|
||||
by the current verification gate.
|
||||
supply it, but commands never print it. This decision's implementation slice verified database
|
||||
behavior through a narrow in-memory database port and unit tests; it deliberately deferred live
|
||||
PostgreSQL evidence to a follow-up slice rather than claiming it at acceptance time.
|
||||
|
||||
Implementation follow-up (2026-07-21): `pnpm test:migrations:integration` now runs the production
|
||||
adapter and migration services against disposable PostgreSQL for lock contention, DDL rollback,
|
||||
history creation, approvals, and sanitized driver failures. The Docker-backed command remains
|
||||
separate from the code-only `pnpm verify` gate.
|
||||
|
||||
The transaction and single-client choices follow node-postgres's
|
||||
[transaction guidance](https://node-postgres.com/features/transactions). The non-blocking lock and
|
||||
@@ -162,6 +166,6 @@ that invocation's schema or history changes committed. Destructive migrations ar
|
||||
a trusted approval provider is composed, by design.
|
||||
|
||||
The all-or-nothing batch can hold locks for longer than per-migration transactions and excludes
|
||||
operations PostgreSQL forbids in a transaction. The current unit-tested database port gives fast,
|
||||
deterministic coverage but cannot prove real server behavior. The next infrastructure-focused
|
||||
slice must add live PostgreSQL tests before production operation is considered complete.
|
||||
operations PostgreSQL forbids in a transaction. The narrow database port continues to provide fast,
|
||||
deterministic unit coverage; the dated implementation follow-up above records the separate live
|
||||
PostgreSQL evidence added after this decision was accepted.
|
||||
|
||||
@@ -69,8 +69,9 @@ validated metadata, explicit destructive-change declarations, and exact SHA-256
|
||||
now compare that inventory with its fixed PostgreSQL history table, produce a read-only plan, and
|
||||
apply the complete pending batch under a session advisory lock and one transaction. Destructive
|
||||
migrations require an exact externally supplied approval bound to module, identity, and checksum.
|
||||
Hermetic integration coverage now exercises those contracts through the production adapter against
|
||||
the pinned PostgreSQL container, including real DDL rollback and advisory-lock concurrency.
|
||||
|
||||
Target architecture, not yet implemented: production approval-provider composition, live
|
||||
PostgreSQL migration-runner integration coverage, Better Auth,
|
||||
Target architecture, not yet implemented: production approval-provider composition, Better Auth,
|
||||
permission enforcement, physical storage-bucket provisioning, Surface SDK, richer public resource
|
||||
and action contracts, API server, dashboard, Builder, and reference feature modules.
|
||||
|
||||
@@ -24,6 +24,7 @@ pnpm migrations:plan # read-only comparison with PostgreSQL applied history
|
||||
pnpm migrations:apply # locked, transactional application of the pending batch
|
||||
pnpm typecheck # strict TypeScript
|
||||
pnpm test # Vitest
|
||||
pnpm test:migrations:integration # production runner against disposable PostgreSQL
|
||||
pnpm build # Turborepo package build graph
|
||||
pnpm verify # every code-only gate; never plan or apply
|
||||
```
|
||||
@@ -63,13 +64,24 @@ The default command composition supplies no destructive approvals. There is no `
|
||||
or source-controlled approval escape hatch; deployments must later compose a trusted external
|
||||
provider that supplies an exact module ID, migration identity, and checksum triple.
|
||||
|
||||
The migration runner currently has comprehensive unit coverage through its narrow database port.
|
||||
Live PostgreSQL tests for real DDL rollback, advisory-lock contention, history-table behavior, and
|
||||
driver cleanup are deliberately deferred and are not part of `pnpm verify` or CI yet.
|
||||
Migration-runner unit tests cover orchestration through its narrow database port. Run the separate
|
||||
live suite when changing migration behavior:
|
||||
|
||||
CI runs `pnpm ci:verify`, which adds Compose configuration validation to `pnpm verify`. A separate
|
||||
CI job runs the live infrastructure smoke test so code-only local verification does not require
|
||||
Docker. The explicit suffix avoids pnpm's built-in `ci` install command.
|
||||
```bash
|
||||
pnpm test:migrations:integration
|
||||
```
|
||||
|
||||
It requires Docker with Compose, starts the pinned PostgreSQL image with a generated throwaway
|
||||
credential and a Docker-assigned loopback port, creates an isolated database for each scenario,
|
||||
and removes its containers, network, databases, and volume afterward. It exercises the production
|
||||
adapter and real PostgreSQL transactions, history, advisory locks, approvals, and sanitized errors.
|
||||
It remains outside the Docker-free `pnpm verify` command.
|
||||
|
||||
CI runs `pnpm ci:verify`, which adds Compose configuration validation to `pnpm verify`. Separate
|
||||
`infrastructure-smoke` and `migration-smoke` jobs run the live service and migration suites. The
|
||||
exact pull-request context to add to `main` branch protection after merge is
|
||||
`CI / migration-smoke (pull_request)`. The explicit `ci:verify` suffix avoids pnpm's built-in `ci`
|
||||
install command.
|
||||
|
||||
## Development infrastructure
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Testing
|
||||
|
||||
Ekklesia separates fast code-level evidence from live infrastructure evidence.
|
||||
|
||||
## Unit and repository verification
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
pnpm verify
|
||||
```
|
||||
|
||||
These commands exercise package tests, repository boundaries, type checking, offline module and
|
||||
migration discovery, formatting, linting, and builds. Migration-runner unit tests use a narrow
|
||||
database/session port to prove deterministic planning, approval, transaction orchestration,
|
||||
cleanup, and sanitized errors. They do not claim to prove PostgreSQL behavior.
|
||||
|
||||
`pnpm verify` is intentionally Docker-free. It never plans or applies migrations against a live
|
||||
database.
|
||||
|
||||
## Live PostgreSQL migration evidence
|
||||
|
||||
Docker and the Docker Compose plugin are required for the live suite:
|
||||
|
||||
```bash
|
||||
pnpm test:migrations:integration
|
||||
```
|
||||
|
||||
The command starts the repository's pinned PostgreSQL image in a unique Compose project, waits for
|
||||
bounded health readiness, runs the production PostgreSQL adapter and migration services, and then
|
||||
removes the container, network, and volume. Each scenario also receives a safely named disposable
|
||||
database. Direct host runs use a Docker-assigned random port bound only to `127.0.0.1`; a
|
||||
containerized Gitea job joins the isolated Compose network and uses the PostgreSQL service name.
|
||||
|
||||
The live suite proves fresh read-only planning, initial and incremental application, genuine
|
||||
no-op behavior, exact applied checksums and history, edited or corrupt history rejection,
|
||||
whole-batch DDL rollback, Drizzle breakpoint execution, real advisory-lock concurrency and
|
||||
timeouts, lock release after failure, exact destructive approvals, and public-error sanitization.
|
||||
It does not make migrations automatic during Core startup.
|
||||
|
||||
CI runs the same command in the dedicated `migration-smoke` job. After this change is merged, add
|
||||
the following pull-request status context to `main` branch protection:
|
||||
|
||||
```text
|
||||
CI / migration-smoke (pull_request)
|
||||
```
|
||||
|
||||
## Safe troubleshooting
|
||||
|
||||
Confirm the prerequisites without printing configuration:
|
||||
|
||||
```bash
|
||||
docker info
|
||||
docker compose version
|
||||
```
|
||||
|
||||
Then rerun `pnpm test:migrations:integration`. The harness emits fixed failure messages and does
|
||||
not dump PostgreSQL logs, SQL, container inspection output, or the generated database URL. Do not
|
||||
enable query logging, print the test environment, run `docker compose config` without redaction,
|
||||
or paste Docker inspection output into an issue. If the Docker daemon itself stopped during
|
||||
cleanup, restore it before rerunning so the harness can create a fresh uniquely named project.
|
||||
@@ -275,9 +275,11 @@ ID, exact identity, and exact checksum. Module metadata cannot grant approval, a
|
||||
wildcard, force, or approve-all option. The default developer command supplies no approvals and
|
||||
therefore refuses destructive pending migrations.
|
||||
|
||||
Runner behavior is unit-tested through a narrow database port. Live PostgreSQL coverage of DDL
|
||||
rollback, advisory-lock contention, and driver behavior is a deferred infrastructure slice; the
|
||||
current verification suite does not claim that coverage.
|
||||
Runner behavior is unit-tested through a narrow database port. The separate
|
||||
`pnpm test:migrations:integration` command now exercises the production adapter against disposable
|
||||
PostgreSQL, including DDL rollback, advisory-lock contention, history behavior, approvals, and
|
||||
sanitized failures. It remains outside the Docker-free `pnpm verify` gate; see the
|
||||
[testing guide](../development/testing.md).
|
||||
|
||||
Core completes discovery before importing any server entrypoint. It then loads and verifies every
|
||||
runtime definition before running hooks, registers all modules dependency-first into an unpublished
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"migrations:validate": "turbo run build && node core/dist/cli/validate-migrations.js",
|
||||
"modules:validate": "turbo run build && node core/dist/cli/validate-modules.js",
|
||||
"test": "turbo run test test:root",
|
||||
"test:migrations:integration": "bash scripts/test-migrations-integration.sh",
|
||||
"test:root": "vitest run --config vitest.config.ts",
|
||||
"test:watch": "vitest",
|
||||
"typecheck": "turbo run typecheck typecheck:root",
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER=false
|
||||
COMPOSE_JOB_NETWORK_CURRENT_CONTAINER=""
|
||||
COMPOSE_JOB_NETWORK_NAME=""
|
||||
COMPOSE_JOB_NETWORK_CONNECTED=false
|
||||
|
||||
compose_job_network_initialize() {
|
||||
COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER=false
|
||||
COMPOSE_JOB_NETWORK_CURRENT_CONTAINER=""
|
||||
COMPOSE_JOB_NETWORK_NAME=""
|
||||
COMPOSE_JOB_NETWORK_CONNECTED=false
|
||||
|
||||
if [[ ! -f /.dockerenv ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ -z "${HOSTNAME:-}" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER=true
|
||||
COMPOSE_JOB_NETWORK_CURRENT_CONTAINER="$HOSTNAME"
|
||||
}
|
||||
|
||||
compose_job_network_connect() {
|
||||
if [[ "$COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER" != "true" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if (($# == 0)); then
|
||||
return 1
|
||||
fi
|
||||
local service="$1"
|
||||
shift
|
||||
if [[ -z "$service" || $# -eq 0 ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local service_container_output
|
||||
if ! service_container_output="$("$@" ps -q "$service" 2>/dev/null)"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local service_containers=()
|
||||
while IFS= read -r container_id; do
|
||||
if [[ -n "$container_id" ]]; then
|
||||
service_containers+=("$container_id")
|
||||
fi
|
||||
done <<<"$service_container_output"
|
||||
if ((${#service_containers[@]} != 1)); then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local service_network_output
|
||||
if ! service_network_output="$(
|
||||
docker inspect \
|
||||
--format '{{range $networkName, $_ := .NetworkSettings.Networks}}{{println $networkName}}{{end}}' \
|
||||
"${service_containers[0]}" 2>/dev/null
|
||||
)"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local service_networks=()
|
||||
while IFS= read -r network_name; do
|
||||
if [[ -n "$network_name" ]]; then
|
||||
service_networks+=("$network_name")
|
||||
fi
|
||||
done <<<"$service_network_output"
|
||||
if ((${#service_networks[@]} != 1)); then
|
||||
return 1
|
||||
fi
|
||||
COMPOSE_JOB_NETWORK_NAME="${service_networks[0]}"
|
||||
|
||||
local current_network_output
|
||||
if ! current_network_output="$(
|
||||
docker inspect \
|
||||
--format '{{range $networkName, $_ := .NetworkSettings.Networks}}{{println $networkName}}{{end}}' \
|
||||
"$COMPOSE_JOB_NETWORK_CURRENT_CONTAINER" 2>/dev/null
|
||||
)"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
while IFS= read -r network_name; do
|
||||
if [[ "$network_name" == "$COMPOSE_JOB_NETWORK_NAME" ]]; then
|
||||
COMPOSE_JOB_NETWORK_CONNECTED=true
|
||||
return 0
|
||||
fi
|
||||
done <<<"$current_network_output"
|
||||
|
||||
COMPOSE_JOB_NETWORK_CONNECTED=true
|
||||
if ! docker network connect \
|
||||
"$COMPOSE_JOB_NETWORK_NAME" \
|
||||
"$COMPOSE_JOB_NETWORK_CURRENT_CONTAINER" >/dev/null 2>&1; then
|
||||
docker network disconnect \
|
||||
"$COMPOSE_JOB_NETWORK_NAME" \
|
||||
"$COMPOSE_JOB_NETWORK_CURRENT_CONTAINER" >/dev/null 2>&1 || true
|
||||
COMPOSE_JOB_NETWORK_CONNECTED=false
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
compose_job_network_disconnect() {
|
||||
if [[ "$COMPOSE_JOB_NETWORK_CONNECTED" != "true" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! docker network disconnect \
|
||||
"$COMPOSE_JOB_NETWORK_NAME" \
|
||||
"$COMPOSE_JOB_NETWORK_CURRENT_CONTAINER" >/dev/null 2>&1; then
|
||||
return 1
|
||||
fi
|
||||
COMPOSE_JOB_NETWORK_CONNECTED=false
|
||||
}
|
||||
@@ -5,6 +5,9 @@ set -Eeuo pipefail
|
||||
repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$repository_root"
|
||||
|
||||
# shellcheck source=scripts/lib/compose-job-network.sh
|
||||
source "$repository_root/scripts/lib/compose-job-network.sh"
|
||||
|
||||
command -v docker >/dev/null || {
|
||||
echo "Docker is required for the infrastructure smoke test." >&2
|
||||
exit 1
|
||||
@@ -50,18 +53,10 @@ compose=(
|
||||
--env-file .env.example
|
||||
--file infrastructure/compose.yaml
|
||||
)
|
||||
running_in_container=false
|
||||
current_container=""
|
||||
compose_network=""
|
||||
connected_to_compose_network=false
|
||||
if [[ -f /.dockerenv ]]; then
|
||||
running_in_container=true
|
||||
if [[ -z "${HOSTNAME:-}" ]]; then
|
||||
echo "HOSTNAME is required when the smoke test runs inside a Docker container." >&2
|
||||
if ! compose_job_network_initialize; then
|
||||
echo "The current Docker job container could not be identified." >&2
|
||||
exit 1
|
||||
fi
|
||||
current_container="$HOSTNAME"
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
local exit_status=$?
|
||||
@@ -70,17 +65,12 @@ cleanup() {
|
||||
if ((exit_status != 0)); then
|
||||
"${compose[@]}" logs --no-color >&2 || true
|
||||
fi
|
||||
if [[ "$connected_to_compose_network" == "true" ]]; then
|
||||
if ! cleanup_output="$(
|
||||
docker network disconnect "$compose_network" "$current_container" 2>&1
|
||||
)"; then
|
||||
echo "Infrastructure smoke network cleanup failed:" >&2
|
||||
echo "$cleanup_output" >&2
|
||||
if ! compose_job_network_disconnect; then
|
||||
echo "Infrastructure smoke network cleanup failed." >&2
|
||||
if ((exit_status == 0)); then
|
||||
exit_status=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if ! cleanup_output="$(
|
||||
"${compose[@]}" down --volumes --remove-orphans --timeout 10 2>&1
|
||||
)"; then
|
||||
@@ -133,46 +123,11 @@ if "${compose[@]}" exec -T \
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$running_in_container" == "true" ]]; then
|
||||
object_storage_container="$("${compose[@]}" ps -q object-storage)"
|
||||
if [[ -z "$object_storage_container" ]]; then
|
||||
echo "Compose did not return the object-storage container ID." >&2
|
||||
if [[ "$COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER" == "true" ]]; then
|
||||
if ! compose_job_network_connect object-storage "${compose[@]}"; then
|
||||
echo "The infrastructure smoke job could not join the isolated storage network." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
network_output="$(
|
||||
docker inspect \
|
||||
--format '{{range $networkName, $_ := .NetworkSettings.Networks}}{{println $networkName}}{{end}}' \
|
||||
"$object_storage_container"
|
||||
)"
|
||||
object_storage_networks=()
|
||||
while IFS= read -r network_name; do
|
||||
if [[ -n "$network_name" ]]; then
|
||||
object_storage_networks+=("$network_name")
|
||||
fi
|
||||
done <<<"$network_output"
|
||||
if ((${#object_storage_networks[@]} != 1)); then
|
||||
echo "Expected object-storage to use exactly one Compose network." >&2
|
||||
exit 1
|
||||
fi
|
||||
compose_network="${object_storage_networks[0]}"
|
||||
|
||||
current_network_output="$(
|
||||
docker inspect \
|
||||
--format '{{range $networkName, $_ := .NetworkSettings.Networks}}{{println $networkName}}{{end}}' \
|
||||
"$current_container"
|
||||
)"
|
||||
already_connected=false
|
||||
while IFS= read -r network_name; do
|
||||
if [[ "$network_name" == "$compose_network" ]]; then
|
||||
already_connected=true
|
||||
break
|
||||
fi
|
||||
done <<<"$current_network_output"
|
||||
if [[ "$already_connected" != "true" ]]; then
|
||||
docker network connect "$compose_network" "$current_container"
|
||||
connected_to_compose_network=true
|
||||
fi
|
||||
s3_endpoint="http://object-storage:8333"
|
||||
else
|
||||
s3_binding="$("${compose[@]}" port object-storage 8333)"
|
||||
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$repository_root"
|
||||
|
||||
# shellcheck source=scripts/lib/compose-job-network.sh
|
||||
source "$repository_root/scripts/lib/compose-job-network.sh"
|
||||
|
||||
command -v docker >/dev/null || {
|
||||
echo "Docker is required for the PostgreSQL migration integration suite." >&2
|
||||
exit 1
|
||||
}
|
||||
command -v pnpm >/dev/null || {
|
||||
echo "pnpm is required for the PostgreSQL migration integration suite." >&2
|
||||
exit 1
|
||||
}
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
echo "Docker Compose is required for the PostgreSQL migration integration suite." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! compose_job_network_initialize; then
|
||||
echo "The current Docker job container could not be identified." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
raw_project="ekklesia-migration-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-0}-${GITHUB_JOB:-job}-$$"
|
||||
project="${raw_project//[^a-zA-Z0-9_-]/-}"
|
||||
project="${project,,}"
|
||||
|
||||
export POSTGRES_DB="ekklesia_migration_test"
|
||||
export POSTGRES_USER="ekklesia_migration_test"
|
||||
export POSTGRES_PASSWORD="temporary-$project-${RANDOM}-${RANDOM}"
|
||||
export POSTGRES_PORT="0"
|
||||
|
||||
compose=(
|
||||
docker compose
|
||||
--project-name "$project"
|
||||
--env-file .env.example
|
||||
--file infrastructure/compose.yaml
|
||||
)
|
||||
|
||||
cleanup() {
|
||||
local exit_status=$?
|
||||
trap - EXIT INT TERM
|
||||
|
||||
if ! compose_job_network_disconnect; then
|
||||
echo "PostgreSQL migration test network cleanup failed." >&2
|
||||
if ((exit_status == 0)); then
|
||||
exit_status=1
|
||||
fi
|
||||
fi
|
||||
if ! "${compose[@]}" down --volumes --remove-orphans --timeout 10 >/dev/null 2>&1; then
|
||||
echo "PostgreSQL migration test infrastructure cleanup failed." >&2
|
||||
if ((exit_status == 0)); then
|
||||
exit_status=1
|
||||
fi
|
||||
fi
|
||||
exit "$exit_status"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
if ! "${compose[@]}" config --quiet >/dev/null 2>&1; then
|
||||
echo "PostgreSQL migration test infrastructure configuration is invalid." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! "${compose[@]}" up --detach --wait --wait-timeout 120 postgres >/dev/null 2>&1; then
|
||||
echo "Disposable PostgreSQL did not become ready." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER" == "true" ]]; then
|
||||
if ! compose_job_network_connect postgres "${compose[@]}"; then
|
||||
echo "The migration test job could not join the isolated PostgreSQL network." >&2
|
||||
exit 1
|
||||
fi
|
||||
database_host="postgres"
|
||||
database_port="5432"
|
||||
else
|
||||
if ! postgres_binding="$("${compose[@]}" port postgres 5432 2>/dev/null)"; then
|
||||
echo "The disposable PostgreSQL loopback port could not be determined." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! "$postgres_binding" =~ ^127\.0\.0\.1:([0-9]+)$ ]]; then
|
||||
echo "The disposable PostgreSQL port is not bound safely to loopback." >&2
|
||||
exit 1
|
||||
fi
|
||||
database_host="127.0.0.1"
|
||||
database_port="${BASH_REMATCH[1]}"
|
||||
if ((database_port < 1 || database_port > 65535)); then
|
||||
echo "The disposable PostgreSQL loopback port is invalid." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
EKKLESIA_MIGRATION_TEST_DATABASE_URL="postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$database_host:$database_port/$POSTGRES_DB" \
|
||||
pnpm exec vitest run --config vitest.migrations.integration.config.ts
|
||||
@@ -21,6 +21,17 @@ interface PackageDocument {
|
||||
scripts?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface WorkflowJob {
|
||||
needs?: string;
|
||||
steps?: WorkflowStep[];
|
||||
"timeout-minutes"?: number;
|
||||
}
|
||||
|
||||
interface WorkflowStep {
|
||||
run?: string;
|
||||
uses?: string;
|
||||
}
|
||||
|
||||
interface TurboDocument {
|
||||
tasks?: Record<string, unknown>;
|
||||
}
|
||||
@@ -64,19 +75,28 @@ describe("development infrastructure", () => {
|
||||
"utf8",
|
||||
);
|
||||
const workflow = parse(source) as {
|
||||
jobs?: {
|
||||
"infrastructure-smoke"?: { steps?: Array<{ run?: string }> };
|
||||
verify?: { steps?: Array<{ run?: string }> };
|
||||
jobs?: Record<string, WorkflowJob>;
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
const commands = workflow.jobs?.verify?.steps?.flatMap((step) => step.run ?? []) ?? [];
|
||||
const infrastructureCommands =
|
||||
workflow.jobs?.["infrastructure-smoke"]?.steps?.flatMap((step) => step.run ?? []) ?? [];
|
||||
const verify = workflow.jobs?.verify;
|
||||
const infrastructure = workflow.jobs?.["infrastructure-smoke"];
|
||||
const migration = workflow.jobs?.["migration-smoke"];
|
||||
const commands = verify?.steps?.flatMap((step) => step.run ?? []) ?? [];
|
||||
const infrastructureCommands = infrastructure?.steps?.flatMap((step) => step.run ?? []) ?? [];
|
||||
const migrationCommands = migration?.steps?.flatMap((step) => step.run ?? []) ?? [];
|
||||
const verifyActions = verify?.steps?.flatMap((step) => step.uses ?? []) ?? [];
|
||||
const migrationActions = migration?.steps?.flatMap((step) => step.uses ?? []) ?? [];
|
||||
|
||||
expect(workflow.jobs?.verify).toBeDefined();
|
||||
expect(workflow.name).toBe("CI");
|
||||
expect(verify).toBeDefined();
|
||||
expect(commands).toContain("pnpm install --frozen-lockfile");
|
||||
expect(commands).toContain("pnpm ci:verify");
|
||||
expect(infrastructureCommands).toContain("bash scripts/smoke-infrastructure.sh");
|
||||
expect(migration?.needs).toBe("verify");
|
||||
expect(migration?.["timeout-minutes"]).toBe(15);
|
||||
expect(migrationActions).toEqual(verifyActions);
|
||||
expect(migrationCommands).toContain("pnpm install --frozen-lockfile");
|
||||
expect(migrationCommands).toContain("pnpm test:migrations:integration");
|
||||
});
|
||||
|
||||
it("routes containerized S3 smoke checks through the isolated Compose network", async () => {
|
||||
@@ -84,18 +104,54 @@ describe("development infrastructure", () => {
|
||||
path.join(repositoryRoot, "scripts", "smoke-infrastructure.sh"),
|
||||
"utf8",
|
||||
);
|
||||
const helper = await readFile(
|
||||
path.join(repositoryRoot, "scripts", "lib", "compose-job-network.sh"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(source).toContain("[[ -f /.dockerenv ]]");
|
||||
expect(source).toContain(`object_storage_container="$("\${compose[@]}" ps -q object-storage)"`);
|
||||
expect(source).toContain("docker inspect");
|
||||
expect(source).toContain('docker network connect "$compose_network" "$current_container"');
|
||||
expect(source).toContain('docker network disconnect "$compose_network" "$current_container"');
|
||||
expect(source).toContain('source "$repository_root/scripts/lib/compose-job-network.sh"');
|
||||
expect(source).toContain(`compose_job_network_connect object-storage "\${compose[@]}"`);
|
||||
expect(source).toContain('s3_endpoint="http://object-storage:8333"');
|
||||
expect(source).toContain('s3_endpoint="http://127.0.0.1:$s3_port"');
|
||||
expect(source).not.toContain("0.0.0.0:${S3_PORT");
|
||||
expect(source.indexOf("docker network disconnect")).toBeLessThan(
|
||||
expect(source.indexOf("compose_job_network_disconnect")).toBeLessThan(
|
||||
source.indexOf(`"\${compose[@]}" down`),
|
||||
);
|
||||
|
||||
expect(helper).toContain("[[ ! -f /.dockerenv ]]");
|
||||
expect(helper).toContain('"$@" ps -q "$service"');
|
||||
expect(helper).toContain("docker inspect");
|
||||
expect(helper).toContain("docker network connect");
|
||||
expect(helper).toContain("docker network disconnect");
|
||||
});
|
||||
|
||||
it("provisions isolated PostgreSQL for the live migration suite", async () => {
|
||||
const packageSource = await readFile(path.join(repositoryRoot, "package.json"), "utf8");
|
||||
const packageDocument = JSON.parse(packageSource) as PackageDocument;
|
||||
const source = await readFile(
|
||||
path.join(repositoryRoot, "scripts", "test-migrations-integration.sh"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
expect(packageDocument.scripts?.["test:migrations:integration"]).toBe(
|
||||
"bash scripts/test-migrations-integration.sh",
|
||||
);
|
||||
expect(source).toContain('source "$repository_root/scripts/lib/compose-job-network.sh"');
|
||||
expect(source).toContain('export POSTGRES_PORT="0"');
|
||||
expect(source).toContain("up --detach --wait --wait-timeout 120 postgres");
|
||||
expect(source).toContain(`compose_job_network_connect postgres "\${compose[@]}"`);
|
||||
expect(source).toContain('database_host="postgres"');
|
||||
expect(source).toContain('database_port="5432"');
|
||||
expect(source).toContain('database_host="127.0.0.1"');
|
||||
expect(source).toContain(
|
||||
'EKKLESIA_MIGRATION_TEST_DATABASE_URL="postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$database_host:$database_port/$POSTGRES_DB"',
|
||||
);
|
||||
expect(source).toContain("--config vitest.migrations.integration.config.ts");
|
||||
expect(source).not.toContain("0.0.0.0");
|
||||
expect(source).not.toContain('compose[@]}" logs');
|
||||
expect(source.indexOf("compose_job_network_disconnect")).toBeLessThan(
|
||||
source.indexOf("down --volumes --remove-orphans"),
|
||||
);
|
||||
});
|
||||
|
||||
it("includes every official platform layer in the workspace", async () => {
|
||||
|
||||
+6
-1
@@ -3,6 +3,11 @@
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["scripts/**/*.ts", "tests/**/*.ts", "vitest.config.ts"],
|
||||
"include": [
|
||||
"scripts/**/*.ts",
|
||||
"tests/**/*.ts",
|
||||
"vitest.config.ts",
|
||||
"vitest.migrations.integration.config.ts"
|
||||
],
|
||||
"exclude": ["node_modules", "tests/fixtures"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
fileParallelism: false,
|
||||
hookTimeout: 30_000,
|
||||
include: ["core/tests/postgres-migration.integration.ts"],
|
||||
passWithNoTests: false,
|
||||
testTimeout: 30_000,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user