From f3550f2396ab5c38b22dd2dab825ccbb70d45472 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Fri, 31 Jul 2026 15:06:21 -0400 Subject: [PATCH 1/2] ci: consolidate image builds into environment-parameterised workflows Four per-environment build workflows collapse into two that take an `environment` input, and prd becomes reachable on both. `build.yml` replaces build-dev.yml + build-stg.yml. A push to main still publishes `stg/wallet-backend:` automatically; dev, stg and prd are all reachable by manual dispatch, which is what allows a specific commit to be exercised in production before a release is cut around it. Its SHA tags never collide with promote-release.yml's `vX.Y.Z` tags, so a verification build cannot overwrite a promoted release image. `build-cnpg-timescaledb.yml` replaces the dev/stg pair, adds prd, validates `pg_major`/`tsdb_version` before anything is pushed, and serialises concurrent dispatches that target the same tag. Both call `stellar/actions/sdf-ecr-login` with no inputs: the action declares only `login-public-ecr`, so the `aws-oidc-role`/`aws-ecr-login-role` values the replaced files passed were never read. --- .../workflows/build-cnpg-timescaledb-dev.yml | 46 ------------ .../workflows/build-cnpg-timescaledb-stg.yml | 46 ------------ .github/workflows/build-cnpg-timescaledb.yml | 72 +++++++++++++++++++ .github/workflows/build-dev.yml | 28 -------- .github/workflows/build-stg.yml | 30 -------- .github/workflows/build.yml | 54 ++++++++++++++ README.md | 39 +++++++++- 7 files changed, 162 insertions(+), 153 deletions(-) delete mode 100644 .github/workflows/build-cnpg-timescaledb-dev.yml delete mode 100644 .github/workflows/build-cnpg-timescaledb-stg.yml create mode 100644 .github/workflows/build-cnpg-timescaledb.yml delete mode 100644 .github/workflows/build-dev.yml delete mode 100644 .github/workflows/build-stg.yml create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build-cnpg-timescaledb-dev.yml b/.github/workflows/build-cnpg-timescaledb-dev.yml deleted file mode 100644 index d795c995f..000000000 --- a/.github/workflows/build-cnpg-timescaledb-dev.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Build and Push CNPG TimescaleDB (Dev) - -on: - workflow_dispatch: - inputs: - pg_major: - description: PostgreSQL major version - default: '17' - required: true - tsdb_version: - description: TimescaleDB version (semver, e.g. 2.28.2) - default: '2.28.2' - required: true - -permissions: - id-token: write - contents: read - -jobs: - build-and-push: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - - name: ECR Login via OIDC - id: ecr-login - uses: stellar/actions/sdf-ecr-login@main - with: - aws-oidc-role: ${{ secrets.AWS_GITHUB_OIDC_ROLE }} - aws-ecr-login-role: ${{ secrets.AWS_ECR_LOGIN_ROLE }} - - - name: Build and push image - env: - ECR_REGISTRY: ${{ steps.ecr-login.outputs.ecr-registry }} - ECR_REPO: dev/cnpg-timescaledb - PG_MAJOR: ${{ inputs.pg_major }} - TSDB_VERSION: ${{ inputs.tsdb_version }} - run: | - TAG="${ECR_REGISTRY}/${ECR_REPO}:pg${PG_MAJOR}-tsdb${TSDB_VERSION}" - echo "Building CNPG TimescaleDB image: ${TAG}" - docker build --pull --platform linux/amd64 \ - -f Dockerfile-timescale-cnpg \ - --build-arg PG_MAJOR=${PG_MAJOR} \ - --build-arg TSDB_VERSION=${TSDB_VERSION} \ - -t "${TAG}" . - docker push "${TAG}" diff --git a/.github/workflows/build-cnpg-timescaledb-stg.yml b/.github/workflows/build-cnpg-timescaledb-stg.yml deleted file mode 100644 index 6450c3a6d..000000000 --- a/.github/workflows/build-cnpg-timescaledb-stg.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Build and Push CNPG TimescaleDB (Stg) - -on: - workflow_dispatch: - inputs: - pg_major: - description: PostgreSQL major version - default: '17' - required: true - tsdb_version: - description: TimescaleDB version (semver, e.g. 2.28.2) - default: '2.28.2' - required: true - -permissions: - id-token: write - contents: read - -jobs: - build-and-push: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - - name: ECR Login via OIDC - id: ecr-login - uses: stellar/actions/sdf-ecr-login@main - with: - aws-oidc-role: ${{ secrets.AWS_GITHUB_OIDC_ROLE }} - aws-ecr-login-role: ${{ secrets.AWS_ECR_LOGIN_ROLE }} - - - name: Build and push image - env: - ECR_REGISTRY: ${{ steps.ecr-login.outputs.ecr-registry }} - ECR_REPO: stg/cnpg-timescaledb - PG_MAJOR: ${{ inputs.pg_major }} - TSDB_VERSION: ${{ inputs.tsdb_version }} - run: | - TAG="${ECR_REGISTRY}/${ECR_REPO}:pg${PG_MAJOR}-tsdb${TSDB_VERSION}" - echo "Building CNPG TimescaleDB image: ${TAG}" - docker build --pull --platform linux/amd64 \ - -f Dockerfile-timescale-cnpg \ - --build-arg PG_MAJOR=${PG_MAJOR} \ - --build-arg TSDB_VERSION=${TSDB_VERSION} \ - -t "${TAG}" . - docker push "${TAG}" diff --git a/.github/workflows/build-cnpg-timescaledb.yml b/.github/workflows/build-cnpg-timescaledb.yml new file mode 100644 index 000000000..0f1d81d99 --- /dev/null +++ b/.github/workflows/build-cnpg-timescaledb.yml @@ -0,0 +1,72 @@ +# ABOUTME: Builds and pushes the CloudNativePG + TimescaleDB database image to the dev, stg or prd +# ABOUTME: ECR namespace, tagged pg-tsdb from the dispatch inputs. +name: Build and Push CNPG TimescaleDB + +on: + workflow_dispatch: + inputs: + environment: + description: Target ECR namespace + required: true + default: dev + type: choice + options: + - dev + - stg + - prd + pg_major: + description: PostgreSQL major version + default: '17' + required: true + tsdb_version: + description: TimescaleDB version (semver, e.g. 2.28.2) + default: '2.28.2' + required: true + +permissions: + id-token: write + contents: read + +concurrency: + group: build-cnpg-timescaledb-${{ inputs.environment }}-pg${{ inputs.pg_major }}-tsdb${{ inputs.tsdb_version }} + cancel-in-progress: false + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Validate inputs + env: + PG_MAJOR: ${{ inputs.pg_major }} + TSDB_VERSION: ${{ inputs.tsdb_version }} + run: | + if [[ ! "$PG_MAJOR" =~ ^[0-9]+$ ]]; then + echo "::error::Invalid pg_major '$PG_MAJOR'. Expected an integer (e.g. 17)." + exit 1 + fi + if [[ ! "$TSDB_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Invalid tsdb_version '$TSDB_VERSION'. Expected MAJOR.MINOR.PATCH (e.g. 2.28.2)." + exit 1 + fi + + - uses: actions/checkout@v5 + + - name: ECR Login via OIDC + id: ecr-login + uses: stellar/actions/sdf-ecr-login@main + + - name: Build and push image + env: + ECR_REGISTRY: ${{ steps.ecr-login.outputs.ecr-registry }} + ECR_REPO: ${{ inputs.environment }}/cnpg-timescaledb + PG_MAJOR: ${{ inputs.pg_major }} + TSDB_VERSION: ${{ inputs.tsdb_version }} + run: | + TAG="${ECR_REGISTRY}/${ECR_REPO}:pg${PG_MAJOR}-tsdb${TSDB_VERSION}" + echo "Building CNPG TimescaleDB image: ${TAG}" + docker build --pull --platform linux/amd64 \ + -f Dockerfile-timescale-cnpg \ + --build-arg PG_MAJOR="${PG_MAJOR}" \ + --build-arg TSDB_VERSION="${TSDB_VERSION}" \ + -t "${TAG}" . + docker push "${TAG}" diff --git a/.github/workflows/build-dev.yml b/.github/workflows/build-dev.yml deleted file mode 100644 index 5adc64087..000000000 --- a/.github/workflows/build-dev.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Build and Push Wallet Backend (Dev) - -on: - workflow_dispatch: - -permissions: - id-token: write - contents: read - -jobs: - build-and-push: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - - name: ECR Login via OIDC - id: ecr-login - uses: stellar/actions/sdf-ecr-login@main - with: - aws-oidc-role: ${{ secrets.AWS_GITHUB_OIDC_ROLE }} - aws-ecr-login-role: ${{ secrets.AWS_ECR_LOGIN_ROLE }} - - - name: Build and push image - env: - TAG: ${{ steps.ecr-login.outputs.ecr-registry }}/dev/wallet-backend:${{ github.sha }} - run: | - make docker-build - make docker-push diff --git a/.github/workflows/build-stg.yml b/.github/workflows/build-stg.yml deleted file mode 100644 index 6a4f9663c..000000000 --- a/.github/workflows/build-stg.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Build and Push Wallet Backend (Stg) - -on: - push: - branches: [main] - workflow_dispatch: - -permissions: - id-token: write - contents: read - -jobs: - build-and-push: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v5 - - - name: ECR Login via OIDC - id: ecr-login - uses: stellar/actions/sdf-ecr-login@main - with: - aws-oidc-role: ${{ secrets.AWS_GITHUB_OIDC_ROLE }} - aws-ecr-login-role: ${{ secrets.AWS_ECR_LOGIN_ROLE }} - - - name: Build and push image - env: - TAG: ${{ steps.ecr-login.outputs.ecr-registry }}/stg/wallet-backend:${{ github.sha }} - run: | - make docker-build - make docker-push diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..5c8f02f37 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,54 @@ +# ABOUTME: Builds and pushes a SHA-tagged wallet-backend image to the dev, stg or prd ECR namespace. +# ABOUTME: Pushes to main publish stg; dev, stg and prd are all reachable by manual dispatch. +name: Build and Push Wallet Backend + +on: + push: + branches: [main] + workflow_dispatch: + inputs: + environment: + description: Target ECR namespace + required: true + default: dev + type: choice + options: + - dev + - stg + - prd + +permissions: + id-token: write + contents: read + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: ECR Login via OIDC + id: ecr-login + uses: stellar/actions/sdf-ecr-login@main + + - name: Resolve target namespace + id: target + env: + EVENT_NAME: ${{ github.event_name }} + ENVIRONMENT: ${{ inputs.environment }} + run: | + # A push to main publishes the staging image; a manual dispatch selects the namespace. + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + namespace="$ENVIRONMENT" + else + namespace="stg" + fi + echo "Target namespace: $namespace" + echo "namespace=$namespace" >> "$GITHUB_OUTPUT" + + - name: Build and push image + env: + TAG: ${{ steps.ecr-login.outputs.ecr-registry }}/${{ steps.target.outputs.namespace }}/wallet-backend:${{ github.sha }} + run: | + make docker-build + make docker-push diff --git a/README.md b/README.md index 43f6e4fa2..f870e632a 100644 --- a/README.md +++ b/README.md @@ -1328,7 +1328,8 @@ This section covers deployment and CI/CD configuration for the wallet-backend. **In this section:** - [Releases](#releases) -- [Continuous builds (dev / stg)](#continuous-builds-dev--stg) +- [Continuous builds](#continuous-builds) +- [Database image (CNPG + TimescaleDB)](#database-image-cnpg--timescaledb) ### Releases @@ -1368,6 +1369,38 @@ The workflows enforce these formats — anything else is rejected before any sid Out of scope for the current setup. All releases are cut from `main`. If you need to release a fix without including newer `main` changes, revert the unwanted commits on `main` first and cut from there. -### Continuous builds (dev / stg) +### Continuous builds -Independent of the release flow, every push to `main` produces a SHA-tagged image at `${ecr-registry}/stg/wallet-backend:${sha}` ([`build-stg.yml`](./.github/workflows/build-stg.yml)). On-demand dev images can be built via [`build-dev.yml`](./.github/workflows/build-dev.yml). +Independent of the release flow, [`build.yml`](./.github/workflows/build.yml) produces SHA-tagged images at `${ecr-registry}//wallet-backend:${sha}`. + +Every push to `main` builds and pushes to **stg** automatically. Any environment can also be built on demand: + +```bash +gh workflow run build.yml -f environment=dev # or stg, prd +``` + +Dispatch runs default to `dev` and to the `main` ref; pass `--ref ` to build a branch. + +#### Two paths into prd + +| Path | Tag | Use | +|------|-----|-----| +| `build.yml` with `environment=prd` | `prd/wallet-backend:${sha}` | Exercise a specific commit in production before cutting a release | +| `promote-release.yml` | `prd/wallet-backend:vX.Y.Z` | The release itself — re-tagged from the validated staging image, digest preserved | + +The tag namespaces are disjoint, so a verification build can never overwrite a promoted release image. + +### Database image (CNPG + TimescaleDB) + +TimescaleDB's official images are incompatible with CloudNativePG, so the database image is built from [`Dockerfile-timescale-cnpg`](./Dockerfile-timescale-cnpg), which layers the TimescaleDB extension onto the CNPG base image. Build it with [`build-cnpg-timescaledb.yml`](./.github/workflows/build-cnpg-timescaledb.yml): + +```bash +gh workflow run build-cnpg-timescaledb.yml \ + -f environment=prd \ + -f pg_major=17 \ + -f tsdb_version=2.28.2 +``` + +This pushes to `${ecr-registry}//cnpg-timescaledb:pg-tsdb`. The CNPG `ImageCatalog` and `Database` resources in the deployment repo must reference that exact tag. + +The build fails rather than producing an image whose TimescaleDB version differs from `tsdb_version`. Both the extension bundle and the loader package are pinned to the requested version — the loader is what supplies `timescaledb.control`, and therefore the `default_version` that a bare `CREATE EXTENSION timescaledb` resolves to — and the assembled image is checked before it is tagged. From bc013962b465f76e8ba7c22c0515a567707e7409 Mon Sep 17 00:00:00 2001 From: Aditya Vyas Date: Fri, 31 Jul 2026 15:06:37 -0400 Subject: [PATCH 2/2] fix(db-image): pin the TimescaleDB loader package so the image tag is honest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TimescaleDB ships two Debian packages: the bundle (`timescaledb-2-postgresql-NN`) carries the versioned `.so` and `.sql` artifacts, while the loader (`timescaledb-2-loader-postgresql-NN`) carries the unversioned loader `.so` and `timescaledb.control`. The control file is what supplies `default_version`, and it is stamped with the *loader's* version. The bundle depends on the loader with `>=`, so pinning only the bundle leaves the loader free to resolve to the newest published release. The build then produces an image whose control file names a version it does not carry: as of today a `pg17-tsdb2.28.2` build picks up the 2.29.0 loader, so `default_version` reads 2.29.0 against 2.28.2 artifacts. Bare `CREATE EXTENSION timescaledb` and bare `ALTER EXTENSION timescaledb UPDATE` then fail with no installation script, `pg_available_extensions` reports a phantom upgrade, and the `version:` pin in the CNPG `Database` resource becomes load-bearing for a fresh bootstrap. Pinning both packages is what Timescale's own installation docs prescribe. Two gates make a recurrence a red build rather than a bootstrap failure. Stage one asserts what apt actually resolved for both packages, which also rejects a longer upstream version the `${TSDB_VERSION}*` glob would otherwise match (2.28.21 for a 2.28.2 request). Stage two asserts the assembled image: the control file's `default_version` and `module_pathname`, the versioned install script, and the versioned, tsl and loader libraries. `TSDB_VERSION` moves to a global ARG so both stages share one default; stage two needs it for the assertion. The `.so` copy glob deliberately keeps every version the bundle ships — a database with an older extension version registered resolves `pg_proc.probin` to that version's library — and now says so. Verified locally: bare `CREATE EXTENSION timescaledb` on the built image yields `default_version = installed_version = 2.28.2` and a working hypertable insert. Dropping the loader pin reproduces the 2.29.0 resolution and fails the stage-one gate; a drifted control file fails the stage-two gate. --- Dockerfile-timescale-cnpg | 53 +++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/Dockerfile-timescale-cnpg b/Dockerfile-timescale-cnpg index 51cbf80e4..33fef07c3 100644 --- a/Dockerfile-timescale-cnpg +++ b/Dockerfile-timescale-cnpg @@ -8,6 +8,7 @@ # docker build -f Dockerfile-timescale-cnpg --build-arg PG_MAJOR=17 --build-arg TSDB_VERSION=2.28.2 -t cnpg-timescaledb . ARG PG_MAJOR=17 +ARG TSDB_VERSION=2.28.2 # --------------------------------------------------------------------------- # Stage 1: Install TimescaleDB via apt to capture extension artifacts @@ -15,7 +16,7 @@ ARG PG_MAJOR=17 FROM ghcr.io/cloudnative-pg/postgresql:${PG_MAJOR}-bookworm AS timescaledb-builder ARG PG_MAJOR -ARG TSDB_VERSION=2.28.2 +ARG TSDB_VERSION USER root @@ -35,12 +36,31 @@ RUN echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg m && curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \ | gpg --dearmor -o /etc/apt/trusted.gpg.d/postgresql.gpg -# Install TimescaleDB pinned to the requested version. +# Install TimescaleDB pinned to the requested version. Two packages are needed +# and both are pinned: +# timescaledb-2-postgresql-NN versioned .so and .sql artifacts +# timescaledb-2-loader-postgresql-NN the unversioned loader .so plus +# timescaledb.control, whose +# default_version and module_pathname are +# stamped with the loader's own version +# The bundle depends on the loader with ">=", so an unpinned loader resolves to +# the newest published release and the control file then names a version whose +# install script and library this image does not carry. # The glob (=${TSDB_VERSION}*) matches the distro suffix automatically -# (e.g., 2.19.3~debian12-1234). +# (e.g., 2.28.2~debian12-1710); the dpkg-query check rejects a longer upstream +# version that the same glob would also match (e.g., 2.28.21). RUN apt-get update && apt-get install -y --no-install-recommends \ timescaledb-2-postgresql-${PG_MAJOR}=${TSDB_VERSION}* \ - && rm -rf /var/lib/apt/lists/* + timescaledb-2-loader-postgresql-${PG_MAJOR}=${TSDB_VERSION}* \ + && rm -rf /var/lib/apt/lists/* \ + && set -eu \ + && for pkg in timescaledb-2-postgresql-${PG_MAJOR} timescaledb-2-loader-postgresql-${PG_MAJOR}; do \ + ver="$(dpkg-query -W -f='${Version}' "$pkg")"; \ + case "$ver" in \ + "${TSDB_VERSION}" | "${TSDB_VERSION}"[~-]*) ;; \ + *) echo "FATAL: $pkg resolved to $ver, expected upstream version ${TSDB_VERSION}" >&2; exit 1 ;; \ + esac; \ + done # --------------------------------------------------------------------------- # Stage 2: Assemble final image — only extension artifacts, no build deps @@ -48,8 +68,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ FROM ghcr.io/cloudnative-pg/postgresql:${PG_MAJOR}-bookworm ARG PG_MAJOR +ARG TSDB_VERSION -# Copy TimescaleDB shared libraries +# Copy TimescaleDB shared libraries. The glob keeps every version the bundle +# ships (timescaledb-.so, -tsl-.so, -invalidations-.so): a database +# with an older extension version registered resolves pg_proc.probin to +# $libdir/timescaledb-, so its library must remain in the image. COPY --from=timescaledb-builder \ /usr/lib/postgresql/${PG_MAJOR}/lib/timescaledb*.so \ /usr/lib/postgresql/${PG_MAJOR}/lib/ @@ -62,8 +86,27 @@ COPY --from=timescaledb-builder \ /usr/share/postgresql/${PG_MAJOR}/extension/timescaledb.control \ /usr/share/postgresql/${PG_MAJOR}/extension/ +# timescaledb.control is what a bare CREATE EXTENSION and ALTER EXTENSION ... +# UPDATE resolve against, so default_version must name a version whose install +# script and versioned library are both present in this image. +RUN set -eu; \ + ext="/usr/share/postgresql/${PG_MAJOR}/extension"; \ + lib="/usr/lib/postgresql/${PG_MAJOR}/lib"; \ + ctl="$ext/timescaledb.control"; \ + want_mp='$libdir/timescaledb-'"${TSDB_VERSION}"; \ + dv="$(grep -E '^[[:space:]]*default_version[[:space:]]*=' "$ctl" | head -n1 | cut -d '=' -f2- | tr -d "[:space:]\"'")"; \ + mp="$(grep -E '^[[:space:]]*module_pathname[[:space:]]*=' "$ctl" | head -n1 | cut -d '=' -f2- | tr -d "[:space:]\"'")"; \ + [ "$dv" = "${TSDB_VERSION}" ] || { echo "FATAL: default_version=$dv in $ctl, expected ${TSDB_VERSION}" >&2; exit 1; }; \ + [ "$mp" = "$want_mp" ] || { echo "FATAL: module_pathname=$mp in $ctl, expected $want_mp" >&2; exit 1; }; \ + [ -f "$ext/timescaledb--${TSDB_VERSION}.sql" ] || { echo "FATAL: missing $ext/timescaledb--${TSDB_VERSION}.sql" >&2; exit 1; }; \ + [ -f "$lib/timescaledb-${TSDB_VERSION}.so" ] || { echo "FATAL: missing $lib/timescaledb-${TSDB_VERSION}.so" >&2; exit 1; }; \ + [ -f "$lib/timescaledb-tsl-${TSDB_VERSION}.so" ] || { echo "FATAL: missing $lib/timescaledb-tsl-${TSDB_VERSION}.so" >&2; exit 1; }; \ + [ -f "$lib/timescaledb.so" ] || { echo "FATAL: missing loader $lib/timescaledb.so" >&2; exit 1; }; \ + echo "timescaledb ${TSDB_VERSION}: control file and artifacts verified" + # Switch to CNPG's unprivileged user (UID 26) USER 26 LABEL org.opencontainers.image.title="CloudNativePG + TimescaleDB (Bookworm)" LABEL org.opencontainers.image.description="PostgreSQL ${PG_MAJOR} with TimescaleDB for CloudNativePG on Kubernetes" +LABEL org.opencontainers.image.version="pg${PG_MAJOR}-tsdb${TSDB_VERSION}"