2a64383af5ab35bbbda691df8807d9a83a98b74c
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
278e66049f |
fix(ci): isolate concurrent runs on a shared Docker daemon and explain readiness failures
CI / verify (pull_request) Successful in 50s
CI / infrastructure-smoke (pull_request) Successful in 14s
CI / migration-smoke (pull_request) Successful in 1m49s
CI / image-build (pull_request) Successful in 19s
CI / deploy-smoke (pull_request) Successful in 41s
PR #51's migration-smoke failed 0.7 s after the build: the suite pinned PostgreSQL to an ephemeral port but inherited S3_PORT=8333 from .env.example, so concurrent runs on one runner (Gitea ignores workflow concurrency) contended for one host port. Compose's own error was discarded, so the failure said only that the services "did not become ready". Isolation, from an audit of every script CI runs: - test-migrations-integration.sh: S3_PORT=0, like POSTGRES_PORT=0. - test-migrations-integration.sh and smoke-infrastructure.sh: project names gain a random suffix; run id plus PID alone is not unique when job containers share low PIDs. - smoke-deploy.sh: the edge /24 is chosen from those no network on the daemon uses, and the first `up` chooses again when a concurrent run wins a race for the same subnet (Docker's address-pool overlap), up to five times. - CI image-build: the image tag includes the run id and attempt, so a pull request and a push of the same commit cannot remove each other's image. - Already isolated: every other published port (ephemeral), the TLS suite's container and network (random), smoke staging directories and image tags (random). Diagnostics: scripts/lib/compose-diagnostics.sh prints the failed `up` command's output, `docker compose ps`, and the last 40 log lines of each service, all redacted (supplied literals, credential-looking NAME=value pairs, URL user information, Authorization headers) with terminal control sequences stripped. All three scripts use it at every readiness check; the infrastructure smoke's unredacted full-log dump is replaced by it. tests/ci-scripts.test.ts pins ephemeral ports, unique project names, the subnet choice, the CI image tag, the diagnostics, and the redaction itself (run through bash). Co-Authored-By: Claude Opus 5.5 <[email protected]> |
||
|
|
b9ca0d560d |
feat: add host-provided module object storage
`manifest.storage.buckets` has existed since manifest version 1 and meant nothing — discovery validated it and no code read it. This turns it into a real capability, which is what stands between Announcements and the module system: that product currently fetches a hardcoded LAN S3 endpoint, parses ListObjectsV2 XML by hand, and hands the browser direct object URLs. - A module declares a logical bucket and receives a scoped facade at `context.storage` — list, head, get, put, delete. The contract names no provider: no endpoint, region, credential, or physical bucket is reachable from it, which is what lets the same module run under a future standalone host over its own or a shared S3-compatible service. - Core's first provider speaks S3 through the AWS SDK. SeaweedFS is the deployment target (ADR 0004), never an API name. Probing the pinned image found `CreateBucket` reporting `BucketAlreadyExists` rather than AWS's `BucketAlreadyOwnedByYou`; treating only the AWS spelling as success would have failed every second boot. A live test pins it. - Logical → physical mapping is deterministic and collision-safe: an installation prefix, the module and bucket ids joined by `--` (which cannot occur inside either identifier, so `a-b`+`c` and `a`+`b-c` cannot collide), and a digest over the full untruncated identity. Two modules declaring `assets` always get separate namespaces; neither can see the physical name. - Declaring a bucket is now a requirement: with no provider configured the module is `blocked · storage_unavailable` through ADR 0028's existing resolver, propagating to required dependents. An installation with no object storage stays a complete deployment. - Binding availability is not provider health. Eligibility asks whether storage is configured, never whether it answered; bucket creation is lazy on first use so a storage outage cannot stop the platform booting; a running module whose store blinks stays running and retries. Nothing deletes a bucket. - Credentials are deployment-supplied, never persisted, never routed through module configuration, and never visible to modules, module UI, diagnostics, or API responses. Provider errors translate to a closed code set carrying no endpoint, bucket, request id, or XML. - Retires ADR 0029's transitional `bucket` configuration key. No manifest version change. The template declares no bucket, so it still runs without S3. See ADR 0030. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
27088fc054 |
fix: run the TLS integration harness under a containerized job
The `migration-smoke` job fails on the Gitea runner at `docker port ekklesia-database-tls-<pid> 5432` with `no public port '5432' published`; the later invalid-URL and `ECONNREFUSED 127.0.0.1:5432` failures all cascade from it. Gitea executes the job inside a container and drives sibling containers through the runner's Docker daemon. The harness held two host-only assumptions, both reproduced in a nested job container and fixed here. Bind-mount source paths are resolved by the daemon against the runner host's filesystem. The certificate directory is created in the job container's `/tmp`, which does not exist there, so the daemon silently creates an empty directory, the entrypoint's `cp` fails, the container exits, and `docker port` reports no published port for an exited container. The material is now transferred with `docker cp` into the created-but-not-yet-started container, which crosses the daemon boundary correctly in both topologies, and only the server leaf and its key are transferred — both CA private keys stay in the harness directory. A published port lands on the runner host's loopback, a different network namespace from the job container's; measured, a container the daemon published at `127.0.0.1:32907` is `ECONNREFUSED` at that address from inside the job container. Under a containerized job the harness now publishes nothing and joins both containers to one isolated user-defined network, reaching PostgreSQL by container DNS on 5432. Direct-host runs keep a random port pinned to loopback via Docker's `127.0.0.1::5432` form. `compose_job_network_job_container` exports the topology decision the integration script already made, so there is still exactly one detector. Every TLS assertion is preserved, and the certificate identity is generated from the host actually connected to — `localhost` directly, the container's DNS name otherwise. The mismatch and chain-only controls use the TLS container's address on the isolated network rather than a hardcoded one, and the certificate carries no IP SAN, so verify-full still fails with `ERR_TLS_CERT_ALTNAME_INVALID`. Correcting one measurement in passing: chain-only verification does not skip the identity check. For an IP host `pg` omits `servername` and hands `tls.connect` a bare socket with no host, and `net.Socket._host` is null for an IP literal, so Node checks the fixed fallback name `localhost` — never the address dialled. A certificate whose only SAN is `DNS:example.invalid`, reached chain-only over `127.0.0.1`, is rejected with `Host: localhost. is not in the cert's altnames`. The direct-host control therefore only ever passed because its SAN was `DNS:localhost`. The SAN set now always includes that fallback name so the control proves the same gap in both topologies, and the stale claim is corrected in the transport doc comment. No production behavior changes. Teardown also removes the container's anonymous volume, the isolated network, and the job container's attachment, after success and failure alike. Co-Authored-By: Claude Opus 5 <[email protected]> |
||
|
|
831a48187c | test: add PostgreSQL migration integration coverage |