name: Base Image Check

# Weekly check for base image digest updates and OS-level vulnerabilities.
# Complements Dependabot (which also watches for digest changes) by adding:
#   1. Trivy vulnerability reporting on the base image
#   2. Detection of newer tag versions (e.g., 1.24 → 1.25)
#   3. Auto-PR creation when the digest changes

on:
  schedule:
    # Every Monday at 06:00 UTC
    - cron: "0 6 * * 1"
  workflow_dispatch: {}

concurrency:
  group: base-image-check
  cancel-in-progress: true

permissions:
  contents: read

env:
  DOCKERFILE: src/main/docker/Dockerfile

jobs:
  check-base-image:
    name: Check Base Image
    runs-on: ubuntu-latest
    permissions:
      contents: write      # git push for PR branch
      pull-requests: write  # gh pr create/edit
      issues: write         # gh issue create

    steps:
      - name: Checkout
        uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

      - name: Parse Dockerfile
        id: parse
        run: |
          # Extract the last FROM line (runtime stage)
          FROM_LINE=$(grep -E '^FROM\s+' "$DOCKERFILE" | tail -1)

          # Parse: FROM image:tag@sha256:hex
          IMAGE=$(echo "$FROM_LINE" | sed -n 's/^FROM[[:space:]]*\([^:]*\):.*/\1/p')
          TAG=$(echo "$FROM_LINE" | sed -n 's/.*:\([^@]*\)@.*/\1/p')
          DIGEST=$(echo "$FROM_LINE" | sed -n 's/.*@\(sha256:[a-f0-9]*\).*/\1/p')

          if [ -z "$IMAGE" ] || [ -z "$TAG" ] || [ -z "$DIGEST" ]; then
            echo "::error::Failed to parse FROM line: ${FROM_LINE}"
            exit 1
          fi

          echo "image=${IMAGE}" >> $GITHUB_OUTPUT
          echo "tag=${TAG}" >> $GITHUB_OUTPUT
          echo "digest=${DIGEST}" >> $GITHUB_OUTPUT

          echo "### 📌 Pinned Base Image" >> $GITHUB_STEP_SUMMARY
          echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
          echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
          echo "| Image | \`${IMAGE}\` |" >> $GITHUB_STEP_SUMMARY
          echo "| Tag | \`${TAG}\` |" >> $GITHUB_STEP_SUMMARY
          echo "| Digest | \`${DIGEST}\` |" >> $GITHUB_STEP_SUMMARY

      - name: Fetch remote digest
        id: remote
        run: |
          set -eo pipefail
          IMAGE="${{ steps.parse.outputs.image }}"
          TAG="${{ steps.parse.outputs.tag }}"
          PINNED="${{ steps.parse.outputs.digest }}"

          # skopeo inspect --raw returns the exact manifest bytes from the registry.
          # SHA256 of those bytes is the content-addressable digest per OCI spec.
          # skopeo is pre-installed on ubuntu-latest runners.
          REMOTE_DIGEST=$(skopeo inspect --raw "docker://${IMAGE}:${TAG}" \
            | sha256sum | awk '{print "sha256:"$1}')

          if [ -z "$REMOTE_DIGEST" ] || [ "$REMOTE_DIGEST" = "sha256:" ]; then
            echo "::error::Failed to fetch remote digest for ${IMAGE}:${TAG}"
            exit 1
          fi

          echo "digest=${REMOTE_DIGEST}" >> $GITHUB_OUTPUT

          if [ "$REMOTE_DIGEST" = "$PINNED" ]; then
            echo "changed=false" >> $GITHUB_OUTPUT
            echo "### ✅ Digest Current" >> $GITHUB_STEP_SUMMARY
            echo "Remote digest matches pinned digest — no update needed." >> $GITHUB_STEP_SUMMARY
          else
            echo "changed=true" >> $GITHUB_OUTPUT
            echo "### 🔄 New Digest Available" >> $GITHUB_STEP_SUMMARY
            echo "| | Digest |" >> $GITHUB_STEP_SUMMARY
            echo "|---|---|" >> $GITHUB_STEP_SUMMARY
            echo "| Pinned | \`${PINNED}\` |" >> $GITHUB_STEP_SUMMARY
            echo "| Remote | \`${REMOTE_DIGEST}\` |" >> $GITHUB_STEP_SUMMARY
          fi

      - name: Check for newer tag
        id: newer
        run: |
          IMAGE="${{ steps.parse.outputs.image }}"
          TAG="${{ steps.parse.outputs.tag }}"

          MAJOR=$(echo "$TAG" | cut -d. -f1)
          MINOR=$(echo "$TAG" | cut -d. -f2)

          # Scan a window of +1 through +5 to catch skipped minor versions
          echo "### 🔍 Newer Tag Check" >> $GITHUB_STEP_SUMMARY
          NEWEST=""
          for OFFSET in 1 2 3 4 5; do
            CANDIDATE="${MAJOR}.$((MINOR + OFFSET))"
            if skopeo inspect "docker://${IMAGE}:${CANDIDATE}" > /dev/null 2>&1; then
              NEWEST="$CANDIDATE"
            fi
          done

          if [ -n "$NEWEST" ]; then
            echo "tag=${NEWEST}" >> $GITHUB_OUTPUT
            echo "exists=true" >> $GITHUB_OUTPUT
            echo "⬆️ **\`${NEWEST}\` is available** — consider upgrading from \`${TAG}\`." >> $GITHUB_STEP_SUMMARY
          else
            echo "exists=false" >> $GITHUB_OUTPUT
            echo "\`${TAG}\` is the latest tag (checked up to \`${MAJOR}.$((MINOR + 5))\`)." >> $GITHUB_STEP_SUMMARY
          fi

      - name: Trivy scan on base image
        uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
        with:
          scan-type: image
          image-ref: "${{ steps.parse.outputs.image }}@${{ steps.remote.outputs.digest }}"
          severity: CRITICAL,HIGH
          exit-code: 0
          ignore-unfixed: false
          format: table

      - name: Create PR for digest update
        id: pr
        if: steps.remote.outputs.changed == 'true'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -eo pipefail
          IMAGE="${{ steps.parse.outputs.image }}"
          TAG="${{ steps.parse.outputs.tag }}"
          OLD_DIGEST="${{ steps.parse.outputs.digest }}"
          NEW_DIGEST="${{ steps.remote.outputs.digest }}"
          BRANCH="chore/update-base-image-digest"

          # Update the digest in the Dockerfile
          sed -i "s|${IMAGE}:${TAG}@${OLD_DIGEST}|${IMAGE}:${TAG}@${NEW_DIGEST}|" "$DOCKERFILE"

          # Verify the replacement actually changed something
          if git diff --quiet "$DOCKERFILE"; then
            echo "::warning::sed replacement produced no diff — skipping PR"
            echo "outcome=noop" >> $GITHUB_OUTPUT
            exit 0
          fi

          # Check if Dependabot already has a PR for this Dockerfile
          DEPENDABOT_PR=$(gh pr list --author 'app/dependabot' --state open \
            --json number,headRefName -q '[.[] | select(.headRefName | startswith("dependabot/docker/"))] | first | .number // empty' 2>/dev/null || echo "")
          if [ -n "$DEPENDABOT_PR" ]; then
            echo "Dependabot PR #${DEPENDABOT_PR} already covers this Dockerfile — skipping"
            echo "### ℹ️ Dependabot PR #${DEPENDABOT_PR} already open for Docker digest" >> $GITHUB_STEP_SUMMARY
            echo "outcome=dependabot" >> $GITHUB_OUTPUT
            exit 0
          fi

          # Configure git for the bot commit
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

          # Check for existing PR from this workflow BEFORE touching branches
          EXISTING=$(gh pr list --head "$BRANCH" --state open --json number -q '.[0].number' 2>/dev/null || echo "")

          if [ -n "$EXISTING" ]; then
            # PR exists — apply update on top of the existing branch (fast-forward, no force-push)
            git checkout -- "$DOCKERFILE"
            git fetch origin "$BRANCH"
            git checkout "$BRANCH"
            # Use a broad digest pattern so sed works regardless of what digest the branch currently has
            sed -i "s|${IMAGE}:${TAG}@sha256:[a-f0-9]*|${IMAGE}:${TAG}@${NEW_DIGEST}|" "$DOCKERFILE"
            if git diff --quiet "$DOCKERFILE"; then
              echo "### ✅ PR #${EXISTING} already has the latest digest" >> $GITHUB_STEP_SUMMARY
              echo "outcome=current" >> $GITHUB_OUTPUT
            else
              git add "$DOCKERFILE"
              git commit -m "chore(docker): update base image digest"
              git push origin "$BRANCH"

              # Update the PR body so reviewers see the current digests
              gh pr edit "$EXISTING" --body "## 🐳 Base Image Digest Update

          The weekly base image check detected a new digest for the pinned base image.

          | | Value |
          |---|---|
          | **Image** | \`${IMAGE}:${TAG}\` |
          | **Old digest** | \`${OLD_DIGEST}\` |
          | **New digest** | \`${NEW_DIGEST}\` |

          Review the Trivy scan in the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).

          > Auto-generated by \`base-image-check.yml\`."

              echo "### ✅ Updated existing PR #${EXISTING}" >> $GITHUB_STEP_SUMMARY
              echo "outcome=updated" >> $GITHUB_OUTPUT
            fi
          else
            # No open PR — safe to delete stale remote branch (nothing to auto-close)
            git push origin --delete "$BRANCH" 2>/dev/null || true
            git checkout -B "$BRANCH"
            git add "$DOCKERFILE"
            git commit -m "chore(docker): update base image digest"
            git push -u origin "$BRANCH"

            PR_URL=$(gh pr create \
              --title "chore(docker): update base image digest" \
              --label "dependencies,security" \
              --body "## 🐳 Base Image Digest Update

          The weekly base image check detected a new digest for the pinned base image.

          | | Value |
          |---|---|
          | **Image** | \`${IMAGE}:${TAG}\` |
          | **Old digest** | \`${OLD_DIGEST}\` |
          | **New digest** | \`${NEW_DIGEST}\` |

          Review the Trivy scan in the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).

          > Auto-generated by \`base-image-check.yml\`.")

            echo "### ✅ Created ${PR_URL}" >> $GITHUB_STEP_SUMMARY
            echo "outcome=created" >> $GITHUB_OUTPUT
          fi

      - name: Notify about newer tag
        if: steps.newer.outputs.exists == 'true'
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          IMAGE="${{ steps.parse.outputs.image }}"
          TAG="${{ steps.parse.outputs.tag }}"
          NEXT="${{ steps.newer.outputs.tag }}"
          TITLE="chore(docker): newer base image tag ${NEXT} available"

          # Don't create duplicate issues (exact title match — gh search is fuzzy)
          ISSUES=$(gh issue list --search "\"${TITLE}\" in:title" --state open --json number,title 2>/dev/null || echo "[]")
          EXISTING=$(echo "$ISSUES" | jq -r --arg t "$TITLE" '[.[] | select(.title == $t) | .number] | first // empty')
          if [ -n "$EXISTING" ]; then
            echo "Issue #${EXISTING} already exists — skipping"
          else
            gh issue create \
              --title "$TITLE" \
              --label "dependencies,security" \
              --body "## ⬆️ Newer Base Image Tag Available

          | | Value |
          |---|---|
          | **Current tag** | \`${TAG}\` |
          | **Available tag** | \`${NEXT}\` |
          | **Image** | \`${IMAGE}\` |

          A newer tag is available for the base image. This may include security patches, JDK updates, or UBI OS updates.

          ### Action Required
          1. Update the \`FROM\` line in \`${DOCKERFILE}\`
          2. Run Trivy locally to verify the new image
          3. Test the build

          > Auto-generated by \`base-image-check.yml\`."
          fi

      - name: Summary
        if: always()
        run: |
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "---" >> $GITHUB_STEP_SUMMARY
          echo "### 📋 Result" >> $GITHUB_STEP_SUMMARY

          OUTCOME="${{ steps.pr.outputs.outcome }}"
          case "$OUTCOME" in
            created)    echo "- 🔄 Digest changed — PR created" >> $GITHUB_STEP_SUMMARY ;;
            updated)    echo "- 🔄 Digest changed — existing PR updated" >> $GITHUB_STEP_SUMMARY ;;
            current)    echo "- ✅ Digest changed but PR already has latest" >> $GITHUB_STEP_SUMMARY ;;
            dependabot) echo "- ℹ️ Digest changed — Dependabot PR already open" >> $GITHUB_STEP_SUMMARY ;;
            noop)       echo "- ✅ Digest current — no action needed" >> $GITHUB_STEP_SUMMARY ;;
            *)          echo "- ✅ Digest current — no action needed" >> $GITHUB_STEP_SUMMARY ;;
          esac

          [ "${{ steps.newer.outputs.exists }}" = "true" ] && echo "- ⬆️ Newer tag \`${{ steps.newer.outputs.tag }}\` detected" >> $GITHUB_STEP_SUMMARY
          echo "- 🔍 Trivy scan completed" >> $GITHUB_STEP_SUMMARY
