diff --git a/.github/dockerfiles/Dockerfile_extension b/.github/dockerfiles/Dockerfile_extension index c0317e502..d60f101a6 100644 --- a/.github/dockerfiles/Dockerfile_extension +++ b/.github/dockerfiles/Dockerfile_extension @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1 # DocumentDB extension image for CNPG ImageVolume mode. # Contains only extension artifacts (.so, .control, .sql, bitcode) and # runtime shared-library dependencies. PostgreSQL is provided by the @@ -6,10 +7,28 @@ # Follows the pattern from: # https://github.com/cloudnative-pg/postgres-extensions-containers # +# The DocumentDB extension is installed from the official DocumentDB APT +# repository (https://documentdb.io/deb). The `postgresql-${PG_MAJOR}-documentdb` +# meta-package pulls in its own runtime dependencies (Citus, RUM, libbson, +# PCRE2, pgvector, PostGIS, ...), so no extra extension packages are installed +# explicitly here. +# +# For CI build mode, a locally-built .deb can be installed instead of the +# published package by passing DOCUMENTDB_DEB_PACKAGE (see below); its runtime +# dependencies are still resolved from the official APT repo. +# # Usage: +# # Install a pinned published version from the APT repo: +# docker build \ +# --build-arg PG_MAJOR=18 \ +# --build-arg DOCUMENTDB_APT_VERSION=0.110-0 \ +# -t documentdb-extension:latest \ +# -f Dockerfile_extension . +# +# # Install a locally-built .deb (CI build mode, unpublished versions): # docker build \ # --build-arg PG_MAJOR=18 \ -# --build-arg DEB_PACKAGE_REL_PATH=packages/documentdb_0.110-0_arm64.deb \ +# --build-arg DOCUMENTDB_DEB_PACKAGE=packages/postgresql-18-documentdb_0.111-0_amd64.deb \ # -t documentdb-extension:latest \ # -f Dockerfile_extension . @@ -17,24 +36,47 @@ ARG BASE=ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie FROM ${BASE} AS builder ARG PG_MAJOR=18 -ARG DEB_PACKAGE_REL_PATH +# Full Debian package version of postgresql-${PG_MAJOR}-documentdb to install +# from the official DocumentDB APT repository, in dashed Debian format (e.g. +# 0.110-0, not dotted 0.110.0 — apt's '=' requires an exact string match). Pin +# this per build for reproducibility. If empty, the latest version in the +# 'stable' channel is used. +ARG DOCUMENTDB_APT_VERSION +# Optional path (relative to the build context) to a locally-built +# postgresql-${PG_MAJOR}-documentdb .deb. When set, the extension is installed +# from this file instead of the APT repo — used by CI build mode to validate +# source-built / unpublished versions. Takes precedence over +# DOCUMENTDB_APT_VERSION. Runtime dependencies (Citus, RUM, pgvector, PostGIS, +# ...) are still resolved from the official APT repo configured below. +ARG DOCUMENTDB_DEB_PACKAGE USER 0 -RUN set -eux && \ +RUN --mount=type=bind,target=/ctx set -eux && \ # Snapshot base image system libraries for later diffing ldconfig -p | awk '{print $NF}' | grep '^/' | sort | uniq > /tmp/base-image-libs.out && \ - # Install pgdg extension packages + # Tools needed to add the official DocumentDB APT repository apt-get update && \ - apt-get install -y --no-install-recommends \ - postgresql-${PG_MAJOR}-cron \ - postgresql-${PG_MAJOR}-pgvector \ - postgresql-${PG_MAJOR}-postgis-3 - -# Install the DocumentDB extension from a pre-built .deb -COPY ${DEB_PACKAGE_REL_PATH} /tmp/documentdb.deb -RUN dpkg -i /tmp/documentdb.deb && \ - rm -f /tmp/documentdb.deb + apt-get install -y --no-install-recommends ca-certificates curl gnupg && \ + # Install the official DocumentDB signing key and APT source + curl -fsSL https://documentdb.io/documentdb-archive-keyring.gpg \ + -o /usr/share/keyrings/documentdb-archive-keyring.gpg && \ + echo "deb [signed-by=/usr/share/keyrings/documentdb-archive-keyring.gpg] https://documentdb.io/deb stable main" \ + > /etc/apt/sources.list.d/documentdb.list && \ + apt-get update && \ + # Install the DocumentDB extension: a locally-built .deb (CI build mode) + # takes precedence, then a pinned APT version, otherwise latest stable. + # Both ARGs are optional; ${VAR:-} keeps them safe under `set -u` when + # Docker does not inject an unset ARG into the RUN environment. + if [ -n "${DOCUMENTDB_DEB_PACKAGE:-}" ]; then \ + apt-get install -y --no-install-recommends "/ctx/${DOCUMENTDB_DEB_PACKAGE}"; \ + elif [ -n "${DOCUMENTDB_APT_VERSION:-}" ]; then \ + apt-get install -y --no-install-recommends \ + "postgresql-${PG_MAJOR}-documentdb=${DOCUMENTDB_APT_VERSION}"; \ + else \ + apt-get install -y --no-install-recommends \ + "postgresql-${PG_MAJOR}-documentdb"; \ + fi # Gather system library dependencies not present in the CNPG base image RUN set -eux && \ diff --git a/.github/workflows/build_documentdb_images.yml b/.github/workflows/build_documentdb_images.yml index 37fb1710c..043a65467 100644 --- a/.github/workflows/build_documentdb_images.yml +++ b/.github/workflows/build_documentdb_images.yml @@ -1,7 +1,8 @@ name: RELEASE - Build DocumentDB Candidate Images # Builds documentdb extension and gateway images from public DocumentDB release artifacts. -# - documentdb image: public deb13 PostgreSQL 18 extension package +# - documentdb image: official DocumentDB extension package from the APT repo +# (https://documentdb.io/deb), installed as postgresql-18-documentdb # - gateway image: public documentdb-local image payload # These images follow the DATABASE version track (documentDbVersion in values.yaml). # For operator/sidecar images, see build_operator_images.yml. @@ -13,17 +14,34 @@ on: description: 'Released DocumentDB version to package (for example 0.110.0)' required: false default: '0.110.0' - documentdb_extension_github_repo: - description: 'GitHub owner/repo for DocumentDB extension releases' + documentdb_apt_version: + description: 'Debian package version of postgresql-18-documentdb to pin, dashed format (e.g. 0.110-0). Leave empty to derive from version.' required: false - default: 'documentdb/documentdb' + default: '' documentdb_gateway_image_repo: description: 'Container image repo for gateway source (without tag)' required: false default: 'ghcr.io/documentdb/documentdb/documentdb-local' - repository_dispatch: - types: [documentdb-release] + workflow_call: + inputs: + version: + description: 'Released DocumentDB version to package (for example 0.110.0)' + required: false + type: string + default: '0.110.0' + documentdb_apt_version: + description: 'Debian package version of postgresql-18-documentdb to pin, dashed format (e.g. 0.110-0). Leave empty to derive from version.' + required: false + type: string + default: '' + outputs: + documentdb_version: + description: 'Resolved DocumentDB version (dotted semver, e.g. 0.110.0)' + value: ${{ jobs.resolve-public-artifacts.outputs.documentdb_version }} + image_tag: + description: 'Candidate image tag produced by this build' + value: ${{ jobs.resolve-public-artifacts.outputs.image_tag }} permissions: packages: write @@ -33,10 +51,10 @@ permissions: env: DEFAULT_DOCUMENTDB_VERSION: '0.110.0' - DOCUMENTDB_EXTENSION_GITHUB_REPO: ${{ github.event.inputs.documentdb_extension_github_repo || 'documentdb/documentdb' }} + DOCUMENTDB_APT_REPO_URL: https://documentdb.io/deb + DOCUMENTDB_APT_KEYRING_URL: https://documentdb.io/documentdb-archive-keyring.gpg DOCUMENTDB_GATEWAY_IMAGE_REPO: ${{ github.event.inputs.documentdb_gateway_image_repo || 'ghcr.io/documentdb/documentdb/documentdb-local' }} - jobs: # --------------------------------------------------------------------------- # Resolve public release artifacts @@ -47,6 +65,7 @@ jobs: outputs: documentdb_version: ${{ steps.version.outputs.documentdb_version }} documentdb_version_dash: ${{ steps.version.outputs.documentdb_version_dash }} + documentdb_apt_version: ${{ steps.version.outputs.documentdb_apt_version }} image_tag: ${{ steps.version.outputs.image_tag }} gateway_source_image: ${{ steps.version.outputs.gateway_source_image }} steps: @@ -54,7 +73,7 @@ jobs: id: version run: | set -euo pipefail - RAW_VERSION="${{ github.event.inputs.version || github.event.client_payload.version || env.DEFAULT_DOCUMENTDB_VERSION }}" + RAW_VERSION="${{ inputs.version || env.DEFAULT_DOCUMENTDB_VERSION }}" if [[ "$RAW_VERSION" =~ ^[0-9]+\.[0-9]+-[0-9]+$ ]]; then VERSION="${RAW_VERSION/-/.}" else @@ -65,27 +84,64 @@ jobs: exit 1 fi VERSION_DASH=$(echo "$VERSION" | sed -E 's/^([0-9]+\.[0-9]+)\.([0-9]+)$/\1-\2/') + # APT package version to pin. The official APT repo serves dashed Debian + # versions (e.g. 0.113-0), not dotted semver, so default to VERSION_DASH. + APT_VERSION="${{ inputs.documentdb_apt_version || '' }}" + if [[ -z "$APT_VERSION" ]]; then + APT_VERSION="$VERSION_DASH" + fi SHORT_SHA=$(echo "$GITHUB_SHA" | cut -c1-7) IMAGE_TAG="${VERSION}-build-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${SHORT_SHA}" GATEWAY_SOURCE_IMAGE="${{ env.DOCUMENTDB_GATEWAY_IMAGE_REPO }}:pg17-${VERSION}" echo "documentdb_version=$VERSION" >> $GITHUB_OUTPUT echo "documentdb_version_dash=$VERSION_DASH" >> $GITHUB_OUTPUT + echo "documentdb_apt_version=$APT_VERSION" >> $GITHUB_OUTPUT echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT echo "gateway_source_image=$GATEWAY_SOURCE_IMAGE" >> $GITHUB_OUTPUT echo "DocumentDB version: $VERSION" - echo "Release tag: v$VERSION_DASH" + echo "APT package version: $APT_VERSION" echo "Candidate image tag: $IMAGE_TAG" echo "Gateway source image: $GATEWAY_SOURCE_IMAGE" - - name: Verify public extension release assets + - name: Verify DocumentDB package is available in the APT index env: - VERSION_DASH: ${{ steps.version.outputs.documentdb_version_dash }} + APT_VERSION: ${{ steps.version.outputs.documentdb_apt_version }} run: | set -euo pipefail + echo "Checking keyring: ${{ env.DOCUMENTDB_APT_KEYRING_URL }}" + curl -fsI -L "${{ env.DOCUMENTDB_APT_KEYRING_URL }}" >/dev/null + # Verify the exact package version is actually present in the index, not + # just that the repo responds. The release that triggered this build and + # the APT channel are published by separate pipelines, so absorb publish + # lag with a bounded retry before failing. for ARCH in amd64 arm64; do - ASSET_URL="https://github.com/${{ env.DOCUMENTDB_EXTENSION_GITHUB_REPO }}/releases/download/v${VERSION_DASH}/deb13-postgresql-18-documentdb_${VERSION_DASH}_${ARCH}.deb" - echo "Checking $ASSET_URL" - curl -fsI -L "$ASSET_URL" >/dev/null + PKG_URL="${{ env.DOCUMENTDB_APT_REPO_URL }}/dists/stable/main/binary-${ARCH}/Packages" + echo "Looking for postgresql-18-documentdb=${APT_VERSION} (${ARCH}) in $PKG_URL" + FOUND="" + for ATTEMPT in 1 2 3 4 5 6; do + # Index may be served plain or gzip-compressed; try both. + PACKAGES=$(curl -fsSL --compressed "$PKG_URL" 2>/dev/null \ + || curl -fsSL "${PKG_URL}.gz" 2>/dev/null | gunzip -c) || PACKAGES="" + # Parse the Debian Packages index stanza-by-stanza (fields are + # blank-line separated and NOT in a fixed order — Version typically + # follows Source, not Package — so a positional grep won't work). + if printf '%s\n' "$PACKAGES" | awk -v pkg="postgresql-18-documentdb" -v ver="$APT_VERSION" ' + /^Package:[[:space:]]/ { p=$2 } + /^Version:[[:space:]]/ { v=$2 } + /^[[:space:]]*$/ { if (p==pkg && v==ver) { found=1; exit }; p=""; v="" } + END { if (p==pkg && v==ver) found=1; exit found?0:1 }'; then + FOUND=yes + break + fi + echo "Not in index yet (attempt ${ATTEMPT}/6); retrying in 30s..." + sleep 30 + done + if [[ -z "$FOUND" ]]; then + echo "postgresql-18-documentdb=${APT_VERSION} (${ARCH}) not found in APT 'stable' index." >&2 + echo "The release may not yet be published to ${{ env.DOCUMENTDB_APT_REPO_URL }}." >&2 + exit 1 + fi + echo "Found postgresql-18-documentdb=${APT_VERSION} (${ARCH})." done - name: Verify public gateway source image @@ -123,32 +179,28 @@ jobs: with: persist-credentials: false - - name: Download public extension package - if: matrix.image.name == 'documentdb' - run: | - set -euo pipefail - mkdir -p packages - DEB_FILE="deb13-postgresql-18-documentdb_${{ needs.resolve-public-artifacts.outputs.documentdb_version_dash }}_${{ matrix.arch }}.deb" - ASSET_URL="https://github.com/${{ env.DOCUMENTDB_EXTENSION_GITHUB_REPO }}/releases/download/v${{ needs.resolve-public-artifacts.outputs.documentdb_version_dash }}/${DEB_FILE}" - curl -fsSL -o "packages/${DEB_FILE}" -L "$ASSET_URL" - ls -lh packages/ - - name: Login to GHCR run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - name: Build and Push ${{ matrix.image.name }} (${{ matrix.arch }}) + env: + # Dockerfile_extension uses RUN --mount, which requires BuildKit. + DOCKER_BUILDKIT: '1' run: | set -euo pipefail + # GHCR repository names must be lowercase; the owner login may contain + # uppercase characters (e.g. on forks), so normalize it. + REPO=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') TAG=${{ env.IMAGE_TAG }}-${{ matrix.arch }} - IMAGE=ghcr.io/${{ github.repository }}/${{ matrix.image.name }}:$TAG + IMAGE=ghcr.io/$REPO/${{ matrix.image.name }}:$TAG BUILD_ARGS="" case "${{ matrix.image.name }}" in documentdb) - DEB_FILE="deb13-postgresql-18-documentdb_${{ needs.resolve-public-artifacts.outputs.documentdb_version_dash }}_${{ matrix.arch }}.deb" - echo "Using deb: $DEB_FILE" - BUILD_ARGS="--build-arg PG_MAJOR=18 --build-arg DEB_PACKAGE_REL_PATH=packages/$DEB_FILE" + APT_VERSION="${{ needs.resolve-public-artifacts.outputs.documentdb_apt_version }}" + echo "Installing postgresql-18-documentdb=$APT_VERSION from official APT repo" + BUILD_ARGS="--build-arg PG_MAJOR=18 --build-arg DOCUMENTDB_APT_VERSION=$APT_VERSION" ;; gateway) echo "Using public gateway source image: ${{ needs.resolve-public-artifacts.outputs.gateway_source_image }}" @@ -177,31 +229,34 @@ jobs: - name: Login to GHCR run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + - name: Compute lowercase repository + run: echo "IMAGE_REPO=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" + - name: Create and Push Manifest run: | - docker manifest create ghcr.io/${{ github.repository }}/${{ matrix.image }}:${{ env.IMAGE_TAG }} \ - --amend ghcr.io/${{ github.repository }}/${{ matrix.image }}:${{ env.IMAGE_TAG }}-amd64 \ - --amend ghcr.io/${{ github.repository }}/${{ matrix.image }}:${{ env.IMAGE_TAG }}-arm64 - docker manifest push ghcr.io/${{ github.repository }}/${{ matrix.image }}:${{ env.IMAGE_TAG }} + docker manifest create ghcr.io/${{ env.IMAGE_REPO }}/${{ matrix.image }}:${{ env.IMAGE_TAG }} \ + --amend ghcr.io/${{ env.IMAGE_REPO }}/${{ matrix.image }}:${{ env.IMAGE_TAG }}-amd64 \ + --amend ghcr.io/${{ env.IMAGE_REPO }}/${{ matrix.image }}:${{ env.IMAGE_TAG }}-arm64 + docker manifest push ghcr.io/${{ env.IMAGE_REPO }}/${{ matrix.image }}:${{ env.IMAGE_TAG }} - name: Install cosign uses: sigstore/cosign-installer@v3.8.2 - name: Sign manifest (keyless) run: | - DIGEST=$(docker buildx imagetools inspect ghcr.io/${{ github.repository }}/${{ matrix.image }}:${{ env.IMAGE_TAG }} \ + DIGEST=$(docker buildx imagetools inspect ghcr.io/${{ env.IMAGE_REPO }}/${{ matrix.image }}:${{ env.IMAGE_TAG }} \ | awk '/^Digest:/ { print $2 }') echo "Signing manifest-list@${DIGEST}" - cosign sign ghcr.io/${{ github.repository }}/${{ matrix.image }}@${DIGEST} -y + cosign sign ghcr.io/${{ env.IMAGE_REPO }}/${{ matrix.image }}@${DIGEST} -y - name: Verify manifest signature (keyless) run: | - DIGEST=$(docker buildx imagetools inspect ghcr.io/${{ github.repository }}/${{ matrix.image }}:${{ env.IMAGE_TAG }} \ + DIGEST=$(docker buildx imagetools inspect ghcr.io/${{ env.IMAGE_REPO }}/${{ matrix.image }}:${{ env.IMAGE_TAG }} \ | awk '/^Digest:/ { print $2 }') cosign verify \ - --certificate-identity "https://github.com/${{ github.repository }}/.github/workflows/build_documentdb_images.yml@${{ github.ref }}" \ + --certificate-identity-regexp "^https://github\.com/${{ github.repository }}/\.github/workflows/build_documentdb_images\.yml@" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ - ghcr.io/${{ github.repository }}/${{ matrix.image }}@${DIGEST} + ghcr.io/${{ env.IMAGE_REPO }}/${{ matrix.image }}@${DIGEST} # --------------------------------------------------------------------------- # Summary @@ -218,7 +273,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "- **DocumentDB Version**: \`${{ needs.resolve-public-artifacts.outputs.documentdb_version }}\`" >> $GITHUB_STEP_SUMMARY echo "- **Candidate Image Tag**: \`${{ needs.resolve-public-artifacts.outputs.image_tag }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **Extension Package Source**: \`https://github.com/${{ env.DOCUMENTDB_EXTENSION_GITHUB_REPO }}/releases/download/v${{ needs.resolve-public-artifacts.outputs.documentdb_version_dash }}/deb13-postgresql-18-documentdb_${{ needs.resolve-public-artifacts.outputs.documentdb_version_dash }}_{amd64,arm64}.deb\`" >> $GITHUB_STEP_SUMMARY + echo "- **Extension Package Source**: \`${{ env.DOCUMENTDB_APT_REPO_URL }}\` (postgresql-18-documentdb=${{ needs.resolve-public-artifacts.outputs.documentdb_apt_version }})" >> $GITHUB_STEP_SUMMARY echo "- **Gateway Source Image**: \`${{ needs.resolve-public-artifacts.outputs.gateway_source_image }}\`" >> $GITHUB_STEP_SUMMARY echo "- **Images**: documentdb, gateway" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/build_images.yml b/.github/workflows/build_images.yml index 1134512bf..f42f5b056 100644 --- a/.github/workflows/build_images.yml +++ b/.github/workflows/build_images.yml @@ -179,6 +179,9 @@ jobs: - name: Build and Push ${{ matrix.image.name }} (${{ matrix.arch }}) if: steps.should_build.outputs.skip != 'true' + env: + # Dockerfile_extension uses RUN --mount, which requires BuildKit. + DOCKER_BUILDKIT: '1' run: | set -euo pipefail TAG=${{ env.IMAGE_TAG }}-${{ matrix.arch }} @@ -199,7 +202,7 @@ jobs: exit 1 fi echo "Using deb: $DEB_FILE" - BUILD_ARGS="--build-arg PG_MAJOR=18 --build-arg DEB_PACKAGE_REL_PATH=packages/$DEB_FILE" + BUILD_ARGS="--build-arg PG_MAJOR=18 --build-arg DOCUMENTDB_DEB_PACKAGE=packages/$DEB_FILE" ;; gateway) GW_DEB=$(ls gateway-context/packages/ | grep -E 'gateway.*\.deb' | head -1) diff --git a/.github/workflows/release_documentdb_images.yml b/.github/workflows/release_documentdb_images.yml index b6e81c086..3ffc81e46 100644 --- a/.github/workflows/release_documentdb_images.yml +++ b/.github/workflows/release_documentdb_images.yml @@ -21,6 +21,22 @@ on: default: true type: boolean + workflow_call: + inputs: + candidate_version: + description: 'Database candidate tag to promote (e.g., 0.111.0-build-123456789-1-deadbee)' + required: true + type: string + version: + description: 'Database image release version (e.g., 0.111.0)' + required: true + type: string + update_defaults: + description: 'Create PR to update default image versions in code' + required: false + default: true + type: boolean + permissions: contents: write packages: write @@ -41,11 +57,14 @@ jobs: - name: Login to GHCR run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + - name: Compute lowercase repository + run: echo "IMAGE_REPO=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV" + - name: Verify candidate exists run: | echo "Verifying ${{ matrix.image }}:${{ inputs.candidate_version }} exists..." docker buildx imagetools inspect \ - ghcr.io/${{ github.repository }}/${{ matrix.image }}:${{ inputs.candidate_version }} + ghcr.io/${{ env.IMAGE_REPO }}/${{ matrix.image }}:${{ inputs.candidate_version }} - name: Retag existing manifest env: @@ -54,8 +73,8 @@ jobs: run: | echo "Promoting ${{ matrix.image }} from $SOURCE_TAG to $TARGET_TAG" docker buildx imagetools create \ - -t ghcr.io/${{ github.repository }}/${{ matrix.image }}:${{ env.TARGET_TAG }} \ - ghcr.io/${{ github.repository }}/${{ matrix.image }}:${{ env.SOURCE_TAG }} + -t ghcr.io/${{ env.IMAGE_REPO }}/${{ matrix.image }}:${{ env.TARGET_TAG }} \ + ghcr.io/${{ env.IMAGE_REPO }}/${{ matrix.image }}:${{ env.SOURCE_TAG }} # --------------------------------------------------------------------------- # Update default versions in code and create PR @@ -121,17 +140,16 @@ jobs: sed -i "s|:${OLD_VERSION}\"|:${NEW_VERSION}\"|g" \ operator/cnpg-plugins/sidecar-injector/internal/config/config_test.go - # 6. Update build workflow defaults - sed -i "s|DEFAULT_DOCUMENTDB_VERSION: '${OLD_VERSION}'|DEFAULT_DOCUMENTDB_VERSION: '${NEW_VERSION}'|" \ - .github/workflows/build_documentdb_images.yml - sed -i "s|default: '${OLD_VERSION}'|default: '${NEW_VERSION}'|g" \ - .github/workflows/build_documentdb_images.yml + # NOTE: Workflow files (build_documentdb_images.yml, + # release_documentdb_images.yml) are intentionally NOT edited here. + # The built-in GITHUB_TOKEN cannot push changes under + # .github/workflows/** (GitHub blocks it without the 'workflow' + # scope, which that token cannot be granted). Their version values + # are only manual-dispatch fallbacks; the watch workflow resolves the + # real version dynamically from upstream releases, so leaving them + # static is harmless. - # 7. Update release workflow default version - sed -i "s|default: '${OLD_VERSION}'|default: '${NEW_VERSION}'|g" \ - .github/workflows/release_documentdb_images.yml - - # 8. Update gateway Dockerfile default source image ARG + # 6. Update gateway Dockerfile default source image ARG sed -i "s|pg17-${OLD_VERSION}|pg17-${NEW_VERSION}|" \ .github/dockerfiles/Dockerfile_gateway_public_image @@ -156,9 +174,13 @@ jobs: - Updated `DEFAULT_DOCUMENTDB_IMAGE` and `DEFAULT_GATEWAY_IMAGE` in `constants.go` - Updated sidecar plugin default gateway image in `config.go` and `config_test.go` - Updated `documentDbVersion` in Helm chart `values.yaml` - - Updated build/release workflow defaults in `build_documentdb_images.yml` and `release_documentdb_images.yml` - Updated gateway Dockerfile default source image in `Dockerfile_gateway_public_image` + > Workflow default-version fallbacks are intentionally left + > unchanged: the built-in `GITHUB_TOKEN` cannot modify files under + > `.github/workflows/**`, and those values are only manual-dispatch + > fallbacks (the real version is resolved dynamically). + ### Image References - `ghcr.io/${{ github.repository }}/documentdb:${{ inputs.version }}` - `ghcr.io/${{ github.repository }}/gateway:${{ inputs.version }}` diff --git a/.github/workflows/test-build-and-package.yml b/.github/workflows/test-build-and-package.yml index 44f36295b..22ec85212 100644 --- a/.github/workflows/test-build-and-package.yml +++ b/.github/workflows/test-build-and-package.yml @@ -47,6 +47,8 @@ jobs: ext_image_tag: ${{ steps.probe.outputs.ext_image_tag }} steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Probe documentdb + gateway images id: probe shell: bash @@ -74,6 +76,23 @@ jobs: exit 0 fi + # Force build mode when the extension Dockerfile changed in this PR, so + # the modified Dockerfile is actually exercised instead of pulling a + # prebuilt image from the registry (registry mode would short-circuit it). + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + BASE_SHA="${{ github.event.pull_request.base.sha }}" + if git cat-file -e "$BASE_SHA" 2>/dev/null; then + if git diff --name-only "$BASE_SHA" HEAD \ + | grep -qx '.github/dockerfiles/Dockerfile_extension'; then + echo ".github/dockerfiles/Dockerfile_extension changed in this PR → forcing build mode." + echo "mode=build" >> "$GITHUB_OUTPUT" + exit 0 + fi + else + echo "::warning::base SHA $BASE_SHA unavailable; skipping Dockerfile-change detection." + fi + fi + OWNER="${{ github.repository_owner }}" # Use the GitHub Packages REST API (works with the workflow's # GITHUB_TOKEN — no anonymous registry token dance required). @@ -399,7 +418,7 @@ jobs: docker buildx build \ --platform linux/${{ matrix.arch }} \ --build-arg PG_MAJOR=18 \ - --build-arg DEB_PACKAGE_REL_PATH=packages/$DEB_FILE \ + --build-arg DOCUMENTDB_DEB_PACKAGE=packages/$DEB_FILE \ --tag "$TARGET" \ --load \ -f .github/dockerfiles/Dockerfile_extension . diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index bfbe52e7b..9fbb4499d 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -23,6 +23,7 @@ on: - 'operator/documentdb-helm-chart/**' - '.github/workflows/test-e2e.yml' - '.github/workflows/test-build-and-package.yml' + - '.github/dockerfiles/**' - '.github/actions/**' workflow_dispatch: inputs: diff --git a/.github/workflows/watch_documentdb_images.yml b/.github/workflows/watch_documentdb_images.yml new file mode 100644 index 000000000..2667500e9 --- /dev/null +++ b/.github/workflows/watch_documentdb_images.yml @@ -0,0 +1,206 @@ +name: WATCH - DocumentDB Releases + +# Reacts to new upstream DocumentDB releases and, when a newer version than the +# operator's current default is published, automatically: +# 1. Builds candidate documentdb + gateway images (build_documentdb_images.yml) +# 2. Promotes them to release tags and opens a "chore: bump DocumentDB images" PR +# (release_documentdb_images.yml) +# +# The version-bump PR is the human gate: a maintainer reviews and merges it, which +# is what actually makes the new version the default for new installs. +# +# Primary trigger: a `repository_dispatch` event of type `documentdb-release`, +# sent by the upstream documentdb/documentdb repository on `release: published`. +# A reference sender workflow lives in docs/designs/upstream-release-dispatch-sender.md. +# A low-frequency cron poll is kept only as a safety net in case the dispatch is +# missed, and `workflow_dispatch` allows manual runs. +# +# Only handles the DATABASE version track (documentDbVersion). Operator/sidecar +# images follow a separate track (build_operator_images.yml / release_operator.yml). + +on: + # Primary trigger: upstream fires this on release: published. + # client_payload: { "version": "0.111.0", "apt_version": "0.111-0" (optional, dashed Debian version) } + repository_dispatch: + types: [documentdb-release] + + # Safety-net poll in case a dispatch event is missed. GitHub's releases/latest + # excludes drafts and pre-releases, so pre-releases never trigger this. + schedule: + - cron: '0 7 * * *' + + workflow_dispatch: + inputs: + version: + description: 'Override upstream version to release (e.g. 0.111.0). Leave empty to auto-detect latest.' + required: false + default: '' + documentdb_apt_version: + description: 'Debian package version of postgresql-18-documentdb to pin, dashed format (e.g. 0.111-0). Leave empty to derive from version.' + required: false + default: '' + dry_run: + description: 'Only detect and report; do not build or open a PR.' + required: false + default: false + type: boolean + +permissions: + contents: write + packages: write + pull-requests: write + id-token: write + +env: + UPSTREAM_REPO: documentdb/documentdb + +jobs: + # --------------------------------------------------------------------------- + # Detect whether a newer upstream release exists + # --------------------------------------------------------------------------- + detect: + name: Detect new DocumentDB release + runs-on: ubuntu-22.04 + outputs: + new_version: ${{ steps.check.outputs.new_version }} + apt_version: ${{ steps.upstream.outputs.apt_version }} + should_release: ${{ steps.check.outputs.should_release }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Resolve latest upstream release + id: upstream + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + # Precedence: repository_dispatch payload > manual override > auto-detect latest. + PAYLOAD_VERSION="${{ github.event.client_payload.version || '' }}" + OVERRIDE="${{ github.event.inputs.version || '' }}" + if [[ -n "$PAYLOAD_VERSION" ]]; then + RAW="$PAYLOAD_VERSION" + echo "Using repository_dispatch payload version: $RAW" + elif [[ -n "$OVERRIDE" ]]; then + RAW="$OVERRIDE" + echo "Using manual version override: $RAW" + else + RAW=$(gh api "repos/${UPSTREAM_REPO}/releases/latest" --jq '.tag_name') + echo "Latest upstream release tag: $RAW" + fi + # Normalize: strip leading 'v', convert dashed (0.110-0) to dotted (0.110.0). + RAW="${RAW#v}" + if [[ "$RAW" =~ ^[0-9]+\.[0-9]+-[0-9]+$ ]]; then + VERSION="${RAW/-/.}" + else + VERSION="$RAW" + fi + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Could not parse a dotted semver from upstream tag '$RAW'" >&2 + exit 1 + fi + # Optional APT package version to pin (payload or manual input). + APT_VERSION="${{ github.event.client_payload.apt_version || github.event.inputs.documentdb_apt_version || '' }}" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "apt_version=$APT_VERSION" >> "$GITHUB_OUTPUT" + echo "Upstream DocumentDB version: $VERSION" + + - name: Read current default version + id: current + run: | + set -euo pipefail + CURRENT=$(sed -nE 's|^[[:space:]]*DEFAULT_DOCUMENTDB_IMAGE[[:space:]]*=.*:([0-9]+\.[0-9]+\.[0-9]+)".*|\1|p' \ + operator/src/internal/utils/constants.go | head -1) + if [[ -z "$CURRENT" ]]; then + echo "Failed to read DEFAULT_DOCUMENTDB_IMAGE from operator/src/internal/utils/constants.go" >&2 + exit 1 + fi + echo "version=$CURRENT" >> "$GITHUB_OUTPUT" + echo "Current default DocumentDB version: $CURRENT" + + - name: Decide whether to release + id: check + run: | + set -euo pipefail + NEW="${{ steps.upstream.outputs.version }}" + CUR="${{ steps.current.outputs.version }}" + echo "new_version=$NEW" >> "$GITHUB_OUTPUT" + + # Not newer than the current default? Nothing to do. + if [[ "$NEW" == "$CUR" ]] || \ + [[ "$(printf '%s\n%s\n' "$CUR" "$NEW" | sort -V | tail -1)" != "$NEW" ]]; then + echo "Upstream $NEW is not newer than current default $CUR. Nothing to do." + echo "should_release=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # Already promoted? Only skip when BOTH the documentdb and gateway + # release images exist for this version. If just one is present (a + # partial/aborted promotion), fall through and rebuild to converge. + echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + # GHCR repository names must be lowercase; the owner login may contain + # uppercase characters (e.g. on forks), so normalize it. + REPO=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]') + DOCDB_EXISTS=false + GW_EXISTS=false + if docker manifest inspect "ghcr.io/${REPO}/documentdb:${NEW}" >/dev/null 2>&1; then + DOCDB_EXISTS=true + fi + if docker manifest inspect "ghcr.io/${REPO}/gateway:${NEW}" >/dev/null 2>&1; then + GW_EXISTS=true + fi + if [[ "$DOCDB_EXISTS" == "true" && "$GW_EXISTS" == "true" ]]; then + echo "Release images documentdb:${NEW} and gateway:${NEW} already exist; version-bump PR is likely pending merge. Skipping." + echo "should_release=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [[ "$DOCDB_EXISTS" == "true" || "$GW_EXISTS" == "true" ]]; then + echo "Partial promotion detected (documentdb=$DOCDB_EXISTS, gateway=$GW_EXISTS); rebuilding to converge." + fi + + if [[ "${{ github.event.inputs.dry_run }}" == "true" ]]; then + echo "Dry run: a newer version $NEW (current $CUR) was detected but no build/PR will be created." + echo "should_release=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + echo "New upstream version $NEW detected (current default $CUR). Proceeding to build + release." + echo "should_release=true" >> "$GITHUB_OUTPUT" + + - name: Detection summary + if: always() + run: | + echo "## DocumentDB Release Watch" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "- **Upstream latest**: \`${{ steps.upstream.outputs.version }}\`" >> "$GITHUB_STEP_SUMMARY" + echo "- **Current default**: \`${{ steps.current.outputs.version }}\`" >> "$GITHUB_STEP_SUMMARY" + echo "- **Action**: ${{ steps.check.outputs.should_release == 'true' && 'Building candidate images and opening version-bump PR' || 'No release needed' }}" >> "$GITHUB_STEP_SUMMARY" + + # --------------------------------------------------------------------------- + # Build candidate images for the new version + # --------------------------------------------------------------------------- + build: + name: Build candidate images + needs: detect + if: needs.detect.outputs.should_release == 'true' + uses: ./.github/workflows/build_documentdb_images.yml + with: + version: ${{ needs.detect.outputs.new_version }} + documentdb_apt_version: ${{ needs.detect.outputs.apt_version }} + secrets: inherit + + # --------------------------------------------------------------------------- + # Promote candidate images and open the version-bump PR + # --------------------------------------------------------------------------- + release: + name: Promote images and open version-bump PR + needs: [detect, build] + if: needs.detect.outputs.should_release == 'true' + uses: ./.github/workflows/release_documentdb_images.yml + with: + candidate_version: ${{ needs.build.outputs.image_tag }} + version: ${{ needs.detect.outputs.new_version }} + update_defaults: true + secrets: inherit diff --git a/AGENTS.md b/AGENTS.md index 9c2cf1f2e..0fdcc3c44 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -298,6 +298,7 @@ Types: - `build_documentdb_images.yml` - Build documentdb/gateway candidate images (database version track) - `release_operator.yml` - Promote operator/sidecar images and publish Helm chart - `release_documentdb_images.yml` - Promote documentdb/gateway images and auto-PR version bumps +- `watch_documentdb_images.yml` - React to upstream documentdb/documentdb releases via `repository_dispatch` (`documentdb-release`, primary) with a daily cron safety-net, then auto-build + open version-bump PR on a new release (sender draft: `docs/designs/upstream-release-dispatch-sender.md`) - `build_images.yml` - [DEPRECATED] Combined image builds (replaced by split workflows above) - `release_images.yml` - [DEPRECATED] Combined release (replaced by split workflows above) - `deploy_docs.yml` - Documentation deployment diff --git a/docs/designs/image-management.md b/docs/designs/image-management.md index 79219b9f2..0b1b71273 100644 --- a/docs/designs/image-management.md +++ b/docs/designs/image-management.md @@ -44,7 +44,7 @@ All images are published to **GitHub Container Registry (GHCR)** under `ghcr.io/ | Image | GHCR Path | Source | Dockerfile | Purpose | |-------|-----------|--------|------------|---------| -| **documentdb** | `.../documentdb` | Public `deb13` PostgreSQL 18 package from `documentdb/documentdb` releases | `.github/dockerfiles/Dockerfile_extension` | DocumentDB PostgreSQL extension files for CNPG ImageVolume mode | +| **documentdb** | `.../documentdb` | Official `postgresql-18-documentdb` package from the DocumentDB APT repo (`https://documentdb.io/deb`) | `.github/dockerfiles/Dockerfile_extension` | DocumentDB PostgreSQL extension files for CNPG ImageVolume mode | | **gateway** | `.../gateway` | Public gateway payload copied from `ghcr.io/documentdb/documentdb/documentdb-local:pg17-` | `.github/dockerfiles/Dockerfile_gateway_public_image` | MongoDB wire-protocol gateway binary (Rust) | ### External Image (Not Built Here) @@ -203,20 +203,20 @@ Builds documentdb extension and gateway images from public DocumentDB release ar | Aspect | Details | |--------|---------| -| **Trigger** | `workflow_dispatch`, `repository_dispatch` (from upstream) | +| **Trigger** | `workflow_dispatch` (manual), `workflow_call` (invoked by `watch_documentdb_images.yml`) | | **Images** | documentdb, gateway | | **Dockerfiles** | `.github/dockerfiles/Dockerfile_extension`, `.github/dockerfiles/Dockerfile_gateway_public_image` | | **Tag pattern** | `{documentdb_version}-build-{run_id}-{attempt}-{sha}` (candidate) | | **Build time** | ~5 minutes (public artifact download + image build) | | **Multi-arch** | amd64 + arm64 → multi-arch manifest | | **Signing** | cosign keyless (OIDC) | -| **Version detection** | Workflow input / repository dispatch payload (defaults to released `0.110.0`) | +| **Version detection** | Workflow input `version` (track) + optional `documentdb_apt_version` (APT pin); defaults to released `0.110.0` | The build process: -1. Resolves the released DocumentDB version to package -2. Downloads the public `deb13` PostgreSQL 18 extension package from `documentdb/documentdb` release assets +1. Resolves the released DocumentDB version to package (and the APT package version to pin) +2. Verifies the official DocumentDB APT repo (`https://documentdb.io/deb`) and signing keyring are reachable 3. Verifies the public multi-arch `documentdb-local:pg17-` image exists -4. Builds `Dockerfile_extension` using the public extension `.deb` (installs pg_cron, pgvector, postgis alongside) +4. Builds `Dockerfile_extension`, installing `postgresql-18-documentdb` from the official APT repo (its meta-package pulls in Citus, RUM, pgvector, PostGIS, etc.) 5. Builds `Dockerfile_gateway_public_image` by copying the gateway binary and runtime files from the public upstream image ### Dockerfile Details @@ -236,7 +236,7 @@ The build process: - **Multi-stage**: 2 stages - **No entrypoint** — this is an ImageVolume source, not a runnable container - Follows the [cloudnative-pg/postgres-extensions-containers](https://github.com/cloudnative-pg/postgres-extensions-containers) pattern -- Installs DocumentDB extension + pg_cron + pgvector + PostGIS +- Installs `postgresql-18-documentdb` from the official DocumentDB APT repo (`https://documentdb.io/deb`); the meta-package pulls in its own dependencies (Citus, RUM, pgvector, PostGIS, ...) - Copies only extension artifacts (`.so`, `.control`, `.sql`, bitcode) and required system libraries - Resolves Debian-alternatives symlinks (they break in ImageVolume mode) @@ -302,6 +302,50 @@ Flow: └── Opens PR: "chore: bump DocumentDB images to 0.111.0" ``` +### Automatic Release Detection (`watch_documentdb_images.yml`) + +Watches the upstream [`documentdb/documentdb`](https://github.com/documentdb/documentdb) +repository for new releases and drives the database track end-to-end without manual +intervention. This is the automation behind keeping new installs on the latest +DocumentDB version. + +The **primary trigger** is a `repository_dispatch` event of type `documentdb-release` +sent by the upstream repo on `release: published`. See +[upstream-release-dispatch-sender.md](upstream-release-dispatch-sender.md) for the +reference sender workflow that lives in `documentdb/documentdb`. A daily cron poll +is kept only as a safety net in case a dispatch is missed. + +| Aspect | Details | +|--------|---------| +| **Trigger** | `repository_dispatch` (`documentdb-release`, primary), `schedule` (daily cron safety-net), `workflow_dispatch` (manual, with optional `version` / `documentdb_apt_version` override and `dry_run`) | +| **Detection** | Reads the dispatch payload version (or upstream `releases/latest` for cron/manual; drafts and pre-releases are excluded by GitHub) and compares against the current `DEFAULT_DOCUMENTDB_IMAGE` in `constants.go` | +| **Chaining** | Calls `build_documentdb_images.yml` then `release_documentdb_images.yml` as reusable workflows (`workflow_call`) | +| **Human gate** | The auto-generated `chore: bump DocumentDB images` PR — a maintainer reviews and merges it to make the new version the default | + +``` +Flow: + 1. detect + ├── Resolve version (dispatch payload → manual override → upstream latest) + ├── Read current default from constants.go + ├── Skip if not newer, if BOTH documentdb+gateway release tags already exist + │ (PR pending), or dry_run; rebuild on partial promotion + └── Output: should_release, new_version, apt_version + + 2. build (uses build_documentdb_images.yml) ── if should_release + └── Output: image_tag (candidate) + + 3. release (uses release_documentdb_images.yml) ── if should_release + ├── candidate_version: + ├── version: + └── update_defaults: true → opens the version-bump PR +``` + +Idempotency: once the release images are promoted, both the `documentdb:` +and `gateway:` tags exist, so subsequent triggers short-circuit until the +bump PR is merged (which advances the default and stops further detection for that +version). A partial promotion (only one of the two tags) does not short-circuit — +the build re-runs to converge. + --- ## Test Pipelines diff --git a/docs/designs/upstream-release-dispatch-sender.md b/docs/designs/upstream-release-dispatch-sender.md new file mode 100644 index 000000000..199824c91 --- /dev/null +++ b/docs/designs/upstream-release-dispatch-sender.md @@ -0,0 +1,109 @@ +# Upstream Release Dispatch Sender (Draft / Reference) + +> **Status:** Reference draft. This workflow does **not** live in this repository — +> it must be added to the upstream **`documentdb/documentdb`** repository. It is +> provided here so the upstream maintainers (e.g. Guanzhou) can wire DocumentDB +> releases to this operator's image automation. + +## What it does + +When `documentdb/documentdb` publishes a GitHub release, this workflow sends a +[`repository_dispatch`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#repository_dispatch) +event of type `documentdb-release` to +`documentdb/documentdb-kubernetes-operator`. That event is the **primary trigger** +for [`watch_documentdb_images.yml`](../../.github/workflows/watch_documentdb_images.yml), +which builds candidate `documentdb` + `gateway` images for the new version and +opens a "chore: bump DocumentDB images" PR (the human merge gate). + +This replaces polling: instead of the operator repo checking upstream every day, +upstream notifies the operator repo the moment a release is published. The daily +cron in `watch_documentdb_images.yml` remains only as a safety net. + +## Prerequisites + +A credential in the **upstream** repo that is allowed to dispatch into the +operator repo. Either: + +- A **fine-grained PAT** (or classic PAT with `repo` scope) belonging to a user + with write access to `documentdb/documentdb-kubernetes-operator`, stored as a + secret named `OPERATOR_DISPATCH_TOKEN`; **or** +- A **GitHub App** installed on the operator repo with `contents: write` (or the + `repository_dispatch` permission), with its token minted at run time. + +A fine-grained PAT scoped to only the operator repo with the **Contents: +read/write** permission is the least-privilege option. + +## Reference workflow (add to `documentdb/documentdb`) + +```yaml +# .github/workflows/notify-operator.yml (in documentdb/documentdb) +name: Notify operator of new release + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + dispatch: + runs-on: ubuntu-latest + steps: + - name: Send repository_dispatch to operator repo + env: + # Least-privilege fine-grained PAT (Contents: write) scoped to the + # operator repo, or a GitHub App token. Do NOT use the default + # GITHUB_TOKEN — it cannot dispatch across repositories. + DISPATCH_TOKEN: ${{ secrets.OPERATOR_DISPATCH_TOKEN }} + run: | + set -euo pipefail + # Normalize tag (strip leading 'v'; dashed 0.110-0 -> dotted 0.110.0). + RAW="${GITHUB_REF_NAME#v}" + if [[ "$RAW" =~ ^[0-9]+\.[0-9]+-[0-9]+$ ]]; then + VERSION="${RAW/-/.}" + else + VERSION="$RAW" + fi + echo "Dispatching documentdb-release for version $VERSION" + # Omit apt_version: the operator derives the dashed Debian version + # (0.111.0 -> 0.111-0) the APT repo expects. Only send apt_version + # explicitly if the APT channel uses a non-standard revision. + curl -fsSL -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${DISPATCH_TOKEN}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/documentdb/documentdb-kubernetes-operator/dispatches \ + -d "$(jq -nc --arg v "$VERSION" \ + '{event_type:"documentdb-release", client_payload:{version:$v}}')" +``` + +## Payload contract + +The operator's `watch_documentdb_images.yml` reads: + +| Field | Required | Meaning | +|-----------------------------|----------|-------------------------------------------------------------------------| +| `client_payload.version` | yes | Dotted semver of the release (e.g. `0.111.0`). Drives image tag + track. | +| `client_payload.apt_version`| no | Debian package version of `postgresql-18-documentdb` to pin, in dashed Debian format (e.g. `0.111-0`). Defaults to the dashed form of `version`. | + +The official APT `stable` channel serves **dashed** Debian versions (e.g. +`0.111-0`), not dotted semver. If omitted, the operator derives `apt_version` +by converting the dotted `version` to its dashed form. Set it explicitly only +if the APT channel publishes a revision that doesn't follow that convention. + +## Testing without upstream + +Until the sender lands upstream, the same flow can be exercised manually: + +- Manually run `watch_documentdb_images.yml` via **workflow_dispatch** with a + `version` (and optional `documentdb_apt_version`) input, or +- Send a one-off dispatch with a token that has access to the operator repo: + + ```bash + curl -fsSL -X POST \ + -H "Authorization: Bearer " \ + -H "Accept: application/vnd.github+json" \ + https://api.github.com/repos/documentdb/documentdb-kubernetes-operator/dispatches \ + -d '{"event_type":"documentdb-release","client_payload":{"version":"0.111.0"}}' + ```