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