From a5258c805924d8a0524540f35f3bceac50f5edb8 Mon Sep 17 00:00:00 2001 From: sebasnallar Date: Tue, 1 Sep 2026 13:27:20 -0300 Subject: [PATCH 1/5] feat(ci): build+push BOTH worker images and register their artifacts One repo, two services, one chained run: release-please cuts the version once; the release-publish-oci chain builds/pushes/registers services/rds-postgres-server, and a parallel job pair does the same for services/rds-postgres-db against the same tag (both ECR Public repos provisioned in iac-null-runtime#109). The release ends with both artifact tables. Dockerfiles follow the scopes-lambda worker pattern (worker-bridge base, aws-cli/gomplate/OpenTofu 1.10.10); the db image adds postgresql16-client for the psql-driven provisioning scripts. Requires: secrets AWS_ROLE_ARN_ECR_PUSH (github-actions-services-postgresql-rds-publisher role, allowed on both images) + ARTIFACT_NP_API_KEY, variable NP_ARTIFACT_NRN. Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 129 ++++++++++++++++++++++++++++++++- Dockerfile.rds-postgres-db | 18 +++++ Dockerfile.rds-postgres-server | 19 +++++ 3 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 Dockerfile.rds-postgres-db create mode 100644 Dockerfile.rds-postgres-server diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8a65e73..ab0870c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,15 +1,140 @@ name: release +# One repo, TWO worker images (rds-postgres-server + rds-postgres-db), one +# chained run: release-please cuts the version once, the release-publish-oci +# chain builds/pushes/registers the SERVER image, and a parallel pair of jobs +# does the same for the DB image against the same tag — both artifacts end up +# in the release notes. +# +# Chained on purpose: release-please creates tags with GITHUB_TOKEN, and +# GitHub never triggers workflows from bot-token events. +# +# Recovery / backfill: dispatch with existing_tag to publish + finalize a tag +# that already exists. +# +# Configure: secrets AWS_ROLE_ARN_ECR_PUSH + ARTIFACT_NP_API_KEY, variable +# NP_ARTIFACT_NRN. on: push: branches: - main + workflow_dispatch: + inputs: + existing_tag: + description: 'Publish + finalize an existing tag (recovery/backfill)' + required: true + type: string permissions: contents: write pull-requests: write + id-token: write # OIDC auth against AWS for the ECR pushes jobs: + # Server image rides the full chain: release-please + build + push + + # artifact registration + release finalize. release: - uses: nullplatform/actions-nullplatform/.github/workflows/release.yml@main - secrets: inherit + uses: nullplatform/actions-nullplatform/.github/workflows/release-publish-oci.yml@main + with: + image_name: services/rds-postgres-server + dockerfile: Dockerfile.rds-postgres-server + existing_tag: ${{ inputs.existing_tag || '' }} + secrets: + aws_role_arn: ${{ secrets.AWS_ROLE_ARN_ECR_PUSH }} + artifact_np_api_key: ${{ secrets.ARTIFACT_NP_API_KEY }} + + # DB image: same tag, its own build/push... + publish-db: + needs: release + if: ${{ !cancelled() && (needs.release.outputs.release_created == 'true' || inputs.existing_tag != '') }} + uses: nullplatform/actions-nullplatform/.github/workflows/docker-build-push-ecr.yml@main + with: + image_name: services/rds-postgres-db + dockerfile: Dockerfile.rds-postgres-db + tag: ${{ inputs.existing_tag || needs.release.outputs.tag_name }} + secrets: + aws_role_arn: ${{ secrets.AWS_ROLE_ARN_ECR_PUSH }} + + # ...and its own artifact registration + release-notes row. Mirrors the + # chain's finalize job — the chain registers one image per run, and this + # repo deliberately ships two from one version. + finalize-db: + name: Register db artifact & append to release + needs: [release, publish-db] + if: ${{ !cancelled() && needs.publish-db.result == 'success' }} + runs-on: ubuntu-24.04 + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.existing_tag || needs.release.outputs.tag_name }} + IMAGE_TAG: ${{ needs.publish-db.outputs.image_tag }} + DIGEST: ${{ needs.publish-db.outputs.image_digest }} + ECR_REGISTRY: public.ecr.aws/nullplatform + IMAGE_NAME: services/rds-postgres-db + steps: + - name: Register oci_image artifact (db) + id: artifact + env: + NULLPLATFORM_API_KEY: ${{ secrets.ARTIFACT_NP_API_KEY }} + NP_ARTIFACT_NRN: ${{ vars.NP_ARTIFACT_NRN }} + run: | + set -o pipefail + if [ -z "$NULLPLATFORM_API_KEY" ]; then + echo "::error::ARTIFACT_NP_API_KEY secret is empty or not set" + exit 1 + fi + if [ -z "$NP_ARTIFACT_NRN" ]; then + echo "::error::NP_ARTIFACT_NRN variable is not set" + exit 1 + fi + curl -fsSL https://cli.nullplatform.com/install.sh | VERSION=alpha-packages sh + export PATH="$HOME/.local/bin:$PATH" + + REG_HOST="${ECR_REGISTRY%%/*}" + REPOSITORY="${ECR_REGISTRY#*/}/$IMAGE_NAME" + OUTPUT=$(np artifact create \ + --nrn "$NP_ARTIFACT_NRN" \ + --type oci_image \ + --registry "$REG_HOST" \ + --repository "$REPOSITORY" \ + --digest "$DIGEST" \ + --visible-to "organization=*" \ + --format json) + echo "$OUTPUT" + ARTIFACT_ID=$(echo "$OUTPUT" | jq -r '.id // empty' || true) + [ -z "$ARTIFACT_ID" ] && ARTIFACT_ID="registered (id unavailable)" + echo "artifact_id=$ARTIFACT_ID" >> "$GITHUB_OUTPUT" + + - name: Append db artifact metadata to the release + if: ${{ !cancelled() }} + env: + ARTIFACT_ID: ${{ steps.artifact.outputs.artifact_id }} + REGISTER_RESULT: ${{ steps.artifact.outcome }} + run: | + IMAGE="$ECR_REGISTRY/$IMAGE_NAME" + case "$REGISTER_RESULT" in + success) ID_ROW="${ARTIFACT_ID}" ;; + *) ID_ROW="registration failed — see run log" ;; + esac + + RELEASE_ID=$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$TAG" --jq '.id' 2>/dev/null || true) + if [ -z "$RELEASE_ID" ]; then + RELEASE_ID=$(gh api "repos/$GITHUB_REPOSITORY/releases" --paginate \ + --jq "[.[] | select(.tag_name==\"$TAG\")][0].id // empty") + fi + if [ -z "$RELEASE_ID" ]; then + echo "::warning::no release found for $TAG; skipping the append" + exit 0 + fi + + # shellcheck disable=SC2016 + SECTION=$(printf '## Artifact (rds-postgres-db)\n\n| | |\n|---|---|\n| Image | `%s` |\n| Digest | `%s` |\n| Pinned reference | `%s` |\n| Artifact ID | `%s` |' \ + "$IMAGE:$IMAGE_TAG" "$DIGEST" "$IMAGE@$DIGEST" "$ID_ROW") + + BODY=$(gh api "repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID" --jq '.body // ""') + if ! printf '%s' "$BODY" | grep -qF "$DIGEST"; then + BODY=$(printf '%s\n\n%s' "$BODY" "$SECTION") + fi + printf '%s' "$BODY" > body.md + gh api -X PATCH "repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID" \ + -F draft=false -F "body=@body.md" > /dev/null + echo "db artifact appended to release $TAG" diff --git a/Dockerfile.rds-postgres-db b/Dockerfile.rds-postgres-db new file mode 100644 index 0000000..9e50d27 --- /dev/null +++ b/Dockerfile.rds-postgres-db @@ -0,0 +1,18 @@ +# syntax=docker/dockerfile:1 +# +# rds-postgres-db service worker image — same base and tooling as the server +# image, plus the postgres client its provisioning scripts drive with psql. +FROM public.ecr.aws/nullplatform/scopes/worker-bridge:1.0.0 + +RUN apk add --no-cache aws-cli gomplate postgresql16-client + +ARG TOFU_VERSION=1.10.10 +ARG TARGETARCH +RUN curl -fsSL "https://github.com/opentofu/opentofu/releases/download/v${TOFU_VERSION}/tofu_${TOFU_VERSION}_linux_${TARGETARCH}.tar.gz" \ + | tar -xz -C /usr/local/bin tofu \ + && tofu version + +COPY . /app/pkg +ENV NP_PACKAGE_NAME=rds-postgres-db \ + NP_SERVICE_PATH=/app/pkg/rds-postgres-db \ + NP_SCOPE_ENTRYPOINT=/app/pkg/rds-postgres-db/entrypoint/entrypoint diff --git a/Dockerfile.rds-postgres-server b/Dockerfile.rds-postgres-server new file mode 100644 index 0000000..2e08d4b --- /dev/null +++ b/Dockerfile.rds-postgres-server @@ -0,0 +1,19 @@ +# syntax=docker/dockerfile:1 +# +# rds-postgres-server service worker image — built on the lean gRPC worker +# bridge, with the cloud tooling the RDS server workflows need baked in. +FROM public.ecr.aws/nullplatform/scopes/worker-bridge:1.0.0 + +RUN apk add --no-cache aws-cli gomplate + +# OpenTofu >= 1.10 (S3 backend with use_lockfile); alpine packages 1.7.x. +ARG TOFU_VERSION=1.10.10 +ARG TARGETARCH +RUN curl -fsSL "https://github.com/opentofu/opentofu/releases/download/v${TOFU_VERSION}/tofu_${TOFU_VERSION}_linux_${TARGETARCH}.tar.gz" \ + | tar -xz -C /usr/local/bin tofu \ + && tofu version + +COPY . /app/pkg +ENV NP_PACKAGE_NAME=rds-postgres-server \ + NP_SERVICE_PATH=/app/pkg/rds-postgres-server \ + NP_SCOPE_ENTRYPOINT=/app/pkg/rds-postgres-server/entrypoint/entrypoint From 4b1bbfca805de28993f132adc3b4cfabbcf51b1f Mon Sep 17 00:00:00 2001 From: sebasnallar Date: Tue, 1 Sep 2026 13:38:07 -0300 Subject: [PATCH 2/5] ci: use the repo's existing NULLPLATFORM_API_KEY for artifact registration Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab0870c..eae8611 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ name: release # Recovery / backfill: dispatch with existing_tag to publish + finalize a tag # that already exists. # -# Configure: secrets AWS_ROLE_ARN_ECR_PUSH + ARTIFACT_NP_API_KEY, variable +# Configure: secrets AWS_ROLE_ARN_ECR_PUSH + NULLPLATFORM_API_KEY (both already set), variable # NP_ARTIFACT_NRN. on: push: @@ -41,7 +41,7 @@ jobs: existing_tag: ${{ inputs.existing_tag || '' }} secrets: aws_role_arn: ${{ secrets.AWS_ROLE_ARN_ECR_PUSH }} - artifact_np_api_key: ${{ secrets.ARTIFACT_NP_API_KEY }} + artifact_np_api_key: ${{ secrets.NULLPLATFORM_API_KEY }} # DB image: same tag, its own build/push... publish-db: @@ -74,7 +74,7 @@ jobs: - name: Register oci_image artifact (db) id: artifact env: - NULLPLATFORM_API_KEY: ${{ secrets.ARTIFACT_NP_API_KEY }} + NULLPLATFORM_API_KEY: ${{ secrets.NULLPLATFORM_API_KEY }} NP_ARTIFACT_NRN: ${{ vars.NP_ARTIFACT_NRN }} run: | set -o pipefail From 0d368ce080ccbec18518d0b852fff0b0d4e563d8 Mon Sep 17 00:00:00 2001 From: sebasnallar Date: Tue, 1 Sep 2026 13:40:47 -0300 Subject: [PATCH 3/5] ci: artifact registration uses its own ARTIFACT_NP_API_KEY NULLPLATFORM_API_KEY serves a different purpose in these repos; the artifact key is dedicated, same convention as scopes-lambda. Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eae8611..ab0870c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ name: release # Recovery / backfill: dispatch with existing_tag to publish + finalize a tag # that already exists. # -# Configure: secrets AWS_ROLE_ARN_ECR_PUSH + NULLPLATFORM_API_KEY (both already set), variable +# Configure: secrets AWS_ROLE_ARN_ECR_PUSH + ARTIFACT_NP_API_KEY, variable # NP_ARTIFACT_NRN. on: push: @@ -41,7 +41,7 @@ jobs: existing_tag: ${{ inputs.existing_tag || '' }} secrets: aws_role_arn: ${{ secrets.AWS_ROLE_ARN_ECR_PUSH }} - artifact_np_api_key: ${{ secrets.NULLPLATFORM_API_KEY }} + artifact_np_api_key: ${{ secrets.ARTIFACT_NP_API_KEY }} # DB image: same tag, its own build/push... publish-db: @@ -74,7 +74,7 @@ jobs: - name: Register oci_image artifact (db) id: artifact env: - NULLPLATFORM_API_KEY: ${{ secrets.NULLPLATFORM_API_KEY }} + NULLPLATFORM_API_KEY: ${{ secrets.ARTIFACT_NP_API_KEY }} NP_ARTIFACT_NRN: ${{ vars.NP_ARTIFACT_NRN }} run: | set -o pipefail From ca887e11833b3de47d8d17b78badc7c0f868ac98 Mon Sep 17 00:00:00 2001 From: sebasnallar Date: Tue, 1 Sep 2026 13:45:34 -0300 Subject: [PATCH 4/5] =?UTF-8?q?ci:=20suppress=20AVD-DS-0002=20with=20ratio?= =?UTF-8?q?nale=20=E2=80=94=20worker-bridge=20runs=20as=20root=20by=20desi?= =?UTF-8?q?gn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same documented suppression scopes-lambda carries; the runtime user is the agent's worker-pod securityContext concern, not a Dockerfile USER. Co-Authored-By: Claude Fable 5 --- .trivyignore | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.trivyignore b/.trivyignore index 5f8c40e..4d0d481 100644 --- a/.trivyignore +++ b/.trivyignore @@ -1,9 +1,7 @@ -# Trivy misconfiguration suppressions for this repo. -# Each entry is intentional and documented; revisit if the module changes. +# Trivy misconfiguration suppressions for this repo (intentional, documented). -# AVD-AWS-0104 — RDS security group allows unrestricted egress (0.0.0.0/0). -# Standard for an RDS instance's own security group (matches the AWS default -# VPC SG behavior): the instance needs outbound access for things like CA -# bundle/extension fetches and Secrets Manager rotation. Inbound is already -# restricted to PostgreSQL (5432) from the VPC's own CIDR blocks. -AVD-AWS-0104 +# AVD-DS-0002 — the worker image builds on the shared worker-bridge base, +# which runs as root by design today (same as scopes-lambda): the runtime +# user is governed by the agent's worker pod securityContext/patches, not by +# a USER directive here. Revisit when the bridge ships a non-root variant. +AVD-DS-0002 From 4dff1cb83ecc25c1db8ffe08506913e5cfc90bfb Mon Sep 17 00:00:00 2001 From: sebasnallar Date: Tue, 1 Sep 2026 13:46:48 -0300 Subject: [PATCH 5/5] ci: suppress pre-existing AVD-AWS-0104 (open egress) with rationale Co-Authored-By: Claude Fable 5 --- .trivyignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.trivyignore b/.trivyignore index 4d0d481..9b19229 100644 --- a/.trivyignore +++ b/.trivyignore @@ -5,3 +5,9 @@ # user is governed by the agent's worker pod securityContext/patches, not by # a USER directive here. Revisit when the bridge ships a non-root variant. AVD-DS-0002 + +# AVD-AWS-0104 — pre-existing service design, not introduced by the CI PR: +# the RDS security groups open egress to 0.0.0.0/0 in both services' +# deployment tofu. Suppressed to keep the gate meaningful for NEW findings; +# flagged for review — restricting egress is a service behavior change. +AVD-AWS-0104