From 424d289854e71927463d881711389f21322fb28f Mon Sep 17 00:00:00 2001 From: Brandon Palm Date: Mon, 27 Apr 2026 13:58:17 -0500 Subject: [PATCH] Pin container image versions and add update automation Replace :latest tags with specific release versions in images.go. openscap-ocp and compliance-operator are pinned to v1.7.0 (the latest release with published container images). k8scontent is pinned to its current commit SHA since the content repo does not publish versioned image tags. Add a GitHub Actions workflow that runs daily to check for new releases, verify images exist in ghcr.io via skopeo, and open PRs to update the pinned versions. A shared script handles the backward search through releases for compliance-operator images. For k8scontent, the workflow inspects :latest to get the current revision SHA and updates if it has changed. --- .github/scripts/find-latest-image.sh | 67 ++++++++++++ .github/workflows/update-image-versions.yml | 110 ++++++++++++++++++++ pkg/utils/images.go | 6 +- 3 files changed, 180 insertions(+), 3 deletions(-) create mode 100755 .github/scripts/find-latest-image.sh create mode 100644 .github/workflows/update-image-versions.yml diff --git a/.github/scripts/find-latest-image.sh b/.github/scripts/find-latest-image.sh new file mode 100755 index 0000000000..53392e32a9 --- /dev/null +++ b/.github/scripts/find-latest-image.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Searches backwards through GitHub releases to find the newest release +# where all specified container images have been published. +# +# Usage: find-latest-image.sh [image2 ...] +# github_repo: GitHub repo in "owner/name" format +# image1..N: Full image references without tags (e.g., ghcr.io/org/name) +# +# Options (via environment variables): +# SEMVER_ONLY=true Skip non-semver tags (default: false) +# FALLBACK_TAG= Tag to return if no release has published images (default: exits 1) +# +# Outputs the matched tag to stdout. All logging goes to stderr. + +GITHUB_REPO="$1" +shift +IMAGES=("$@") + +if [ ${#IMAGES[@]} -eq 0 ]; then + echo "Usage: find-latest-image.sh [image2 ...]" >&2 + exit 1 +fi + +SEMVER_ONLY="${SEMVER_ONLY:-false}" +FALLBACK_TAG="${FALLBACK_TAG:-}" + +RELEASES=$(curl -sf --retry 3 --retry-delay 5 \ + "https://api.github.com/repos/${GITHUB_REPO}/releases?per_page=20" \ + | jq -r '.[].tag_name') + +for RELEASE_TAG in $RELEASES; do + if [ "$SEMVER_ONLY" = "true" ]; then + if ! echo "$RELEASE_TAG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+'; then + echo "Skipping non-semver tag: $RELEASE_TAG" >&2 + continue + fi + fi + + echo "Checking images for ${GITHUB_REPO} release ${RELEASE_TAG}..." >&2 + ALL_FOUND=true + for IMAGE in "${IMAGES[@]}"; do + if ! skopeo inspect --override-arch amd64 --override-os linux \ + "docker://${IMAGE}:${RELEASE_TAG}" > /dev/null 2>&1; then + ALL_FOUND=false + break + fi + done + + if [ "$ALL_FOUND" = "true" ]; then + echo "Found images for ${GITHUB_REPO} release ${RELEASE_TAG}" >&2 + echo "$RELEASE_TAG" + exit 0 + else + echo "Images not found for ${RELEASE_TAG}, trying next..." >&2 + fi +done + +if [ -n "$FALLBACK_TAG" ]; then + echo "No versioned images found, using fallback: ${FALLBACK_TAG}" >&2 + echo "$FALLBACK_TAG" + exit 0 +fi + +echo "No release with published images found for ${GITHUB_REPO}" >&2 +exit 1 diff --git a/.github/workflows/update-image-versions.yml b/.github/workflows/update-image-versions.yml new file mode 100644 index 0000000000..398b98360e --- /dev/null +++ b/.github/workflows/update-image-versions.yml @@ -0,0 +1,110 @@ +name: Update image versions + +on: + schedule: + - cron: '17 6 * * *' + workflow_dispatch: {} + +permissions: + contents: write + pull-requests: write + +env: + REGISTRY: ghcr.io/complianceascode + IMAGES_FILE: pkg/utils/images.go + +jobs: + update-images: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install skopeo + run: | + sudo apt-get update + sudo apt-get install -y skopeo + + - name: Get latest compliance-operator release with published images + id: co-release + run: | + CO_TAG=$(SEMVER_ONLY=true .github/scripts/find-latest-image.sh \ + ComplianceAsCode/compliance-operator \ + "$REGISTRY/compliance-operator" \ + "$REGISTRY/openscap-ocp") + echo "tag=$CO_TAG" >> "$GITHUB_OUTPUT" + + - name: Get latest k8scontent revision + id: k8scontent-release + run: | + K8S_TAG=$(skopeo inspect --override-arch amd64 --override-os linux \ + "docker://$REGISTRY/k8scontent:latest" \ + | jq -r '.Labels["org.opencontainers.image.revision"]') + + if [ -z "$K8S_TAG" ] || [ "$K8S_TAG" = "null" ]; then + echo "Failed to get k8scontent revision from :latest" + exit 1 + fi + + echo "Verifying k8scontent:${K8S_TAG} exists..." + skopeo inspect --override-arch amd64 --override-os linux \ + "docker://$REGISTRY/k8scontent:${K8S_TAG}" > /dev/null + + echo "tag=$K8S_TAG" >> "$GITHUB_OUTPUT" + + - name: Check for version changes + id: check + run: | + CO_TAG="${{ steps.co-release.outputs.tag }}" + K8S_TAG="${{ steps.k8scontent-release.outputs.tag }}" + + OPENSCAP_CUR=$(grep 'openscap-ocp:' "$IMAGES_FILE" | sed 's/.*openscap-ocp:\([^"]*\).*/\1/') + OPERATOR_CUR=$(grep 'compliance-operator:' "$IMAGES_FILE" | sed 's/.*compliance-operator:\([^"]*\).*/\1/') + K8S_CUR=$(grep 'k8scontent:' "$IMAGES_FILE" | sed 's/.*k8scontent:\([^"]*\).*/\1/') + + echo "Current: openscap-ocp=$OPENSCAP_CUR compliance-operator=$OPERATOR_CUR k8scontent=$K8S_CUR" + echo "Resolved: openscap-ocp=$CO_TAG compliance-operator=$CO_TAG k8scontent=$K8S_TAG" + + echo "openscap=$OPENSCAP_CUR" >> "$GITHUB_OUTPUT" + echo "operator=$OPERATOR_CUR" >> "$GITHUB_OUTPUT" + echo "k8scontent=$K8S_CUR" >> "$GITHUB_OUTPUT" + + CHANGED="false" + if [ "$CO_TAG" != "$OPENSCAP_CUR" ] || [ "$CO_TAG" != "$OPERATOR_CUR" ] || [ "$K8S_TAG" != "$K8S_CUR" ]; then + CHANGED="true" + fi + echo "changed=$CHANGED" >> "$GITHUB_OUTPUT" + + - name: Update image versions + if: steps.check.outputs.changed == 'true' + run: | + CO_TAG="${{ steps.co-release.outputs.tag }}" + K8S_TAG="${{ steps.k8scontent-release.outputs.tag }}" + + sed -i 's|openscap-ocp:[^"]*|openscap-ocp:'"${CO_TAG}"'|' "$IMAGES_FILE" + sed -i 's|compliance-operator:[^"]*|compliance-operator:'"${CO_TAG}"'|' "$IMAGES_FILE" + sed -i 's|k8scontent:[^"]*|k8scontent:'"${K8S_TAG}"'|' "$IMAGES_FILE" + + echo "Updated $IMAGES_FILE:" + grep -A2 'componentDefaults' "$IMAGES_FILE" | tail -5 + + - name: Create pull request + if: steps.check.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v7 + with: + commit-message: | + Update container image versions + + openscap-ocp: ${{ steps.check.outputs.openscap }} -> ${{ steps.co-release.outputs.tag }} + compliance-operator: ${{ steps.check.outputs.operator }} -> ${{ steps.co-release.outputs.tag }} + k8scontent: ${{ steps.check.outputs.k8scontent }} -> ${{ steps.k8scontent-release.outputs.tag }} + title: "Update container image versions" + body: | + Automated update of pinned container image versions in `pkg/utils/images.go`. + + | Image | Previous | New | + |-------|----------|-----| + | openscap-ocp | `${{ steps.check.outputs.openscap }}` | `${{ steps.co-release.outputs.tag }}` | + | compliance-operator | `${{ steps.check.outputs.operator }}` | `${{ steps.co-release.outputs.tag }}` | + | k8scontent | `${{ steps.check.outputs.k8scontent }}` | `${{ steps.k8scontent-release.outputs.tag }}` | + branch: automated/update-image-versions + delete-branch: true diff --git a/pkg/utils/images.go b/pkg/utils/images.go index bbab648ef8..7b4fc2bf4f 100644 --- a/pkg/utils/images.go +++ b/pkg/utils/images.go @@ -14,9 +14,9 @@ var componentDefaults = []struct { defaultImage string envVar string }{ - {"ghcr.io/complianceascode/openscap-ocp:latest", "RELATED_IMAGE_OPENSCAP"}, - {"ghcr.io/complianceascode/compliance-operator:latest", "RELATED_IMAGE_OPERATOR"}, - {"ghcr.io/complianceascode/k8scontent:latest", "RELATED_IMAGE_PROFILE"}, + {"ghcr.io/complianceascode/openscap-ocp:v1.7.0", "RELATED_IMAGE_OPENSCAP"}, + {"ghcr.io/complianceascode/compliance-operator:v1.7.0", "RELATED_IMAGE_OPERATOR"}, + {"ghcr.io/complianceascode/k8scontent:b01ffe68cc1320ee472408798bc56d83cfbfb1f7", "RELATED_IMAGE_PROFILE"}, } // GetComponentImage returns a full image pull spec for a given component