From 2c23d7f9b30af8c9d4c6fd294556502b9b97f492 Mon Sep 17 00:00:00 2001 From: kzangeli Date: Wed, 2 Sep 2026 12:16:55 +0200 Subject: [PATCH] ci: a mongo image with an oplog, so CI can run the HA tests at all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HA cache sync rides on a mongo CHANGE STREAM, and a change stream reads the oplog - which a standalone mongod does not have. coraine's harness knows that and detects it: corTestParams.sh probes isMaster.setName and, on a standalone, ha_cache_sync.test simply leaves the run set. Silently. Nothing in a CI log says the HA paths went unexercised, and every CI job has run a standalone mongo:8.0 since the workflows existed. So 108 lines and 10 functions of coraine were never entered in CI - and the nightly's published coverage figure reported them as untested CODE rather than as an environment nobody had stood up. doc/coverage.md said exactly that, in as many words, and it was describing our own CI rather than the broker. The fix cannot live in a workflow: a `services:` block passes docker-create OPTIONS but not a COMMAND, so --replSet cannot be appended to mongo:8.0 from there, and the job container has mongosh but no mongod of its own to start instead. Hence an image whose whole content is the command line. One member, because this is not a test of replication - it is the smallest thing that has an oplog. The set is NOT initiated here: the member host has to be the name the clients use, and the image cannot know it. That is one mongosh call in the consumer, next to the wait-for-database it already does. Published on the same immutable - tag as the other two, from the same run, so adopting a set of images stays one bump. The verify step asserts the property consumers actually depend on - it comes up, it initiates, and isMaster then reports setName rs0, which is the very probe the harness makes. ⚠️ quay.io/seamware/mongo-rs does not exist yet: the first push creates it, and it has to be made PUBLIC like the other two, or the service container cannot be pulled. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TGatXwrHx1CreL49sCuS37 --- .github/workflows/baseimage.yml | 57 +++++++++++++++++++++++++++++++++ docker/Dockerfile.mongo-rs | 40 +++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 docker/Dockerfile.mongo-rs diff --git a/.github/workflows/baseimage.yml b/.github/workflows/baseimage.yml index f0eb7d3..32ece71 100644 --- a/.github/workflows/baseimage.yml +++ b/.github/workflows/baseimage.yml @@ -26,6 +26,7 @@ on: paths: - docker/Dockerfile.ci - docker/Dockerfile.ci-nightly + - docker/Dockerfile.mongo-rs - .github/workflows/baseimage.yml push: branches: @@ -33,6 +34,7 @@ on: paths: - docker/Dockerfile.ci - docker/Dockerfile.ci-nightly + - docker/Dockerfile.mongo-rs - .github/workflows/baseimage.yml workflow_dispatch: @@ -106,6 +108,60 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max + # + # The mongo service image, and it is a service image rather than a base: + # nothing builds inside it. It is mongo:8.0 with --replSet on the command + # line, because a workflow `services:` block cannot pass a command and the + # HA cache sync needs an oplog to watch. It rides along here because this + # is where a Dockerfile in this repo gets published, not because it has + # anything to do with the build environment. + # + # Same immutable - tag as the other two, from the same run, so + # adopting a set of images is one bump rather than three. + # + - name: Build and push the mongo replica-set image + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile.mongo-rs + push: ${{ github.event_name != 'pull_request' }} + tags: quay.io/seamware/mongo-rs:${{ steps.tags.outputs.date_sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # + # The property every consumer depends on, asserted here so a consumer does + # not discover it: the image comes up, the set INITIATES, and isMaster then + # reports a setName - which is the exact probe coraine's harness uses to + # decide whether the HA tests are in the run set. + # + - name: Verify the mongo image is an initiable replica set + if: github.event_name != 'pull_request' + run: | + set -eu + img=quay.io/seamware/mongo-rs:${{ steps.tags.outputs.date_sha }} + docker run -d --name mongo-rs-probe "$img" + trap 'docker rm -f mongo-rs-probe >/dev/null 2>&1 || true' EXIT + + for i in $(seq 1 30); do + docker exec mongo-rs-probe mongosh --quiet --eval 'db.runCommand({ping:1}).ok' >/dev/null 2>&1 && break + [ "$i" = 30 ] && { echo "mongod never answered"; exit 1; } + sleep 2 + done + + docker exec mongo-rs-probe mongosh --quiet --eval \ + 'rs.initiate({_id: "rs0", members: [{_id: 0, host: "localhost:27017"}]})' + + for i in $(seq 1 30); do + [ "$(docker exec mongo-rs-probe mongosh --quiet --eval 'db.adminCommand({isMaster:1}).ismaster')" = true ] && break + [ "$i" = 30 ] && { echo "the set never elected a primary"; exit 1; } + sleep 2 + done + + name=$(docker exec mongo-rs-probe mongosh --quiet --eval 'db.adminCommand({isMaster:1}).setName') + [ "$name" = rs0 ] || { echo "setName is '$name', expected rs0"; exit 1; } + echo "single-node replica set rs0 is PRIMARY" + # # The Dockerfile's last layer already asserts the environment is complete. # This repeats it against the built image so the check also covers a @@ -132,6 +188,7 @@ jobs: echo "Published:" echo "- \`quay.io/seamware/${{ env.IMAGE_NAME }}:${{ steps.tags.outputs.date_sha }}\`" echo "- \`quay.io/seamware/${{ env.IMAGE_NAME }}-nightly:${{ steps.tags.outputs.date_sha }}\`" + echo "- \`quay.io/seamware/mongo-rs:${{ steps.tags.outputs.date_sha }}\`" echo echo "To adopt, in coraine: set the image tag in .github/workflows/{ci,nightly}.yml to \`${{ steps.tags.outputs.date_sha }}\`" else diff --git a/docker/Dockerfile.mongo-rs b/docker/Dockerfile.mongo-rs new file mode 100644 index 0000000..c471c4d --- /dev/null +++ b/docker/Dockerfile.mongo-rs @@ -0,0 +1,40 @@ +# Dockerfile.mongo-rs - mongo:8.0, started as a single-node REPLICA SET. +# +# WHAT THIS IS: the stock upstream image with one thing changed - the command. +# No packages, no configuration, no data. If it ever needs a second line of +# content, it is the wrong shape and should be reconsidered. +# +# WHY IT EXISTS: the HA cache sync (`--high-availability mongo`) rides on a +# mongo CHANGE STREAM, and a change stream reads the oplog - which a standalone +# mongod does not have. The harness detects that (corTestParams.sh probes +# isMaster.setName) and leaves ha_cache_sync.test out of the run set on a +# standalone, silently: nothing in the log says the HA paths went unexercised. +# +# Every CI job ran a standalone, so nothing ever entered them - 108 lines and +# 10 functions of coraine, which the published coverage figure then reported as +# untested CODE rather than as an environment we had not stood up. +# +# WHY IT CANNOT BE DONE IN THE WORKFLOW: a GitHub Actions `services:` block +# passes docker-create OPTIONS but not a COMMAND, so `--replSet` cannot be +# appended to mongo:8.0 from a workflow, and the job container has mongosh but +# no mongod of its own to start instead. Two lines here, and every consumer +# keeps using an ordinary service container. +# +# WHAT THIS IMAGE DELIBERATELY DOES NOT DO: initiate the set. A mongod started +# with --replSet and never initiated answers every write with +# NotYetInitialized, so somebody must call rs.initiate() once it is up - but the +# member host has to be the name the CLIENTS use to reach it, and this image +# cannot know that name. It is one mongosh call in the consumer, beside the +# wait-for-database it already does. +# +# ONE MEMBER is the whole point. This is not a test of replication; it is the +# smallest thing that has an oplog. +# +FROM mongo:8.0 + +# +# The upstream entrypoint prepends `mongod` only when the command starts with a +# dash, so the binary is named explicitly. --bind_ip_all because a replica-set +# member is addressed by the name its clients use, not by localhost. +# +CMD ["mongod", "--replSet", "rs0", "--bind_ip_all"]