diff --git a/docs/development/repository-workflow.md b/docs/development/repository-workflow.md index 77e72b4..11379e7 100644 --- a/docs/development/repository-workflow.md +++ b/docs/development/repository-workflow.md @@ -62,6 +62,9 @@ pnpm infrastructure:smoke The smoke test verifies PostgreSQL password authentication and authenticated S3 list, put, get, head, and delete operations. It also proves invalid database passwords, anonymous object access, and invalid S3 signatures are rejected. Its containers, network, and volumes are removed on exit. +When the smoke script itself runs in a Docker-based CI job, it temporarily joins that job container +to the isolated Compose network and reaches object storage by its service name. Direct host runs +continue to use Docker-assigned random ports published only on loopback. PostgreSQL and the S3 endpoint bind to `127.0.0.1` only. Compose uses persistent named volumes; `infrastructure:down` preserves data. The S3 service creates the configured development bucket diff --git a/scripts/smoke-infrastructure.sh b/scripts/smoke-infrastructure.sh index ec08cb5..2f986aa 100755 --- a/scripts/smoke-infrastructure.sh +++ b/scripts/smoke-infrastructure.sh @@ -50,6 +50,18 @@ 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 + exit 1 + fi + current_container="$HOSTNAME" +fi cleanup() { local exit_status=$? @@ -58,6 +70,17 @@ 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 ((exit_status == 0)); then + exit_status=1 + fi + fi + fi if ! cleanup_output="$( "${compose[@]}" down --volumes --remove-orphans --timeout 10 2>&1 )"; then @@ -110,13 +133,56 @@ if "${compose[@]}" exec -T \ exit 1 fi -s3_binding="$("${compose[@]}" port object-storage 8333)" -s3_port="${s3_binding##*:}" -if [[ ! "$s3_port" =~ ^[0-9]+$ ]]; then - echo "Compose returned an invalid S3 port binding: $s3_binding" >&2 - exit 1 +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 + 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)" + s3_port="${s3_binding##*:}" + if [[ ! "$s3_port" =~ ^[0-9]+$ ]]; then + echo "Compose returned an invalid S3 port binding: $s3_binding" >&2 + exit 1 + fi + s3_endpoint="http://127.0.0.1:$s3_port" fi -s3_endpoint="http://127.0.0.1:$s3_port" signed_curl=( curl --silent diff --git a/tests/infrastructure.test.ts b/tests/infrastructure.test.ts index 91da13c..7a5a7d5 100644 --- a/tests/infrastructure.test.ts +++ b/tests/infrastructure.test.ts @@ -79,6 +79,25 @@ describe("development infrastructure", () => { expect(infrastructureCommands).toContain("bash scripts/smoke-infrastructure.sh"); }); + it("routes containerized S3 smoke checks through the isolated Compose network", async () => { + const source = await readFile( + path.join(repositoryRoot, "scripts", "smoke-infrastructure.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('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( + source.indexOf(`"\${compose[@]}" down`), + ); + }); + it("includes every official platform layer in the workspace", async () => { const source = await readFile(path.join(repositoryRoot, "pnpm-workspace.yaml"), "utf8"); const workspace = parse(source) as WorkspaceDocument;