Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 149 additions & 35 deletions .github/workflows/deployment-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ jobs:
outputs:
image-only-check: ${{ steps.check-changes.outputs.result }}
new-images: ${{ steps.check-changes.outputs.images }}
old-images: ${{ steps.check-changes.outputs.old-images }}
steps:
- name: Checkout code
uses: actions/checkout@v4
Expand All @@ -276,8 +277,10 @@ jobs:

if [ "$CHANGED_FILES" = "" ] || [ "$CHANGED_FILES" = "[]" ]; then
echo "No files to validate"
echo "result=pass" >> "$GITHUB_OUTPUT"
echo "images=[]" >> "$GITHUB_OUTPUT"
{
echo "result=pass"
echo "images=[]"
} >> "$GITHUB_OUTPUT"
exit 0
fi

Expand Down Expand Up @@ -324,22 +327,37 @@ jobs:
else
echo "✅ Only image changed: $OLD_IMAGE → $NEW_IMAGE"
echo "$NEW_IMAGE" >> /tmp/new_images.txt
echo "$OLD_IMAGE" >> /tmp/old_images.txt
fi
echo ""
done

if [ -f /tmp/validation_failed.txt ]; then
echo "result=fail" >> "$GITHUB_OUTPUT"
echo "images=[]" >> "$GITHUB_OUTPUT"
{
echo "result=fail"
echo "images=[]"
echo "old-images=[]"
} >> "$GITHUB_OUTPUT"
exit 1
else
if [ -f /tmp/new_images.txt ]; then
IMAGES_JSON=$(jq -R -s -c 'split("\n") | map(select(length > 0)) | unique' < /tmp/new_images.txt)
echo "images=$IMAGES_JSON" >> "$GITHUB_OUTPUT"
else
echo "images=[]" >> "$GITHUB_OUTPUT"
fi
echo "result=pass" >> "$GITHUB_OUTPUT"
# Use block redirect to satisfy shellcheck SC2129
{
if [ -f /tmp/new_images.txt ]; then
IMAGES_JSON=$(jq -R -s -c 'split("\n") | map(select(length > 0)) | unique' < /tmp/new_images.txt)
echo "images=$IMAGES_JSON"
else
echo "images=[]"
fi

if [ -f /tmp/old_images.txt ]; then
OLD_IMAGES_JSON=$(jq -R -s -c 'split("\n") | map(select(length > 0)) | unique' < /tmp/old_images.txt)
echo "old-images=$OLD_IMAGES_JSON"
else
echo "old-images=[]"
fi

echo "result=pass"
} >> "$GITHUB_OUTPUT"
echo "✅ All files have only image field changes"
fi

Expand All @@ -359,6 +377,7 @@ jobs:
id: validate
run: |
NEW_IMAGES='${{ needs.validate-image-only-changed.outputs.new-images }}'
OLD_IMAGES='${{ needs.validate-image-only-changed.outputs.old-images }}'
ALLOWED_REPOS='${{ inputs.allowed_image_repositories }}'
VERSION_PATTERN='${{ inputs.allowed_version_pattern }}'
VERIFY_EXISTENCE='${{ inputs.verify_image_existence }}'
Expand All @@ -369,14 +388,21 @@ jobs:
exit 0
fi

# Create arrays to map new images to old images by index
# Using mapfile to satisfy shellcheck SC2207
mapfile -t OLD_IMAGES_ARRAY < <(echo "$OLD_IMAGES" | jq -r '.[]')

INDEX=0
echo "$NEW_IMAGES" | jq -r '.[]' | while IFS= read -r image; do
echo "=================================================="
echo "Validating image: $image"
echo "=================================================="

# 1. Validate format (repo/name:tag)
if ! [[ "$image" =~ ^[a-zA-Z0-9_/-]+:[a-zA-Z0-9._-]+$ ]]; then
echo "❌ Invalid image format: $image"
echo "❌ Image format validation failed"
echo " Image: $image"
echo " REASON: Invalid image format (expected format: repository/name:tag)"
echo "false" > /tmp/validation_failed.txt
continue
fi
Expand All @@ -385,6 +411,7 @@ jobs:
# 2. Extract repository and tag
REPO="${image%:*}"
TAG="${image##*:}"
echo " Full image: $image"
echo " Repository: $REPO"
echo " Tag: $TAG"

Expand All @@ -395,14 +422,37 @@ jobs:
for allowed_repo in "${ALLOWED[@]}"; do
# Trim whitespace
allowed_repo=$(echo "$allowed_repo" | xargs)
if [[ "$REPO" == "$allowed_repo" ]]; then

# Extract base repository name (handle both with and without registry prefix)
# Examples:
# mirror.gcr.io/dotcms/dotcms -> dotcms/dotcms
# gcr.io/project/dotcms/dotcms -> dotcms/dotcms
# dotcms/dotcms -> dotcms/dotcms
BASE_REPO="$REPO"
if [[ "$REPO" =~ / ]]; then
# If REPO contains registry (has multiple slashes or starts with known registries)
if [[ "$REPO" =~ ^[a-z0-9.-]+\.[a-z]{2,}/.*/ ]] || [[ "$REPO" =~ ^gcr\.io/ ]] || [[ "$REPO" =~ ^.*\.gcr\.io/ ]]; then
# Extract everything after the registry domain
BASE_REPO="${REPO#*/}"
# If there are still slashes, get the last two parts (org/repo)
if [[ "$BASE_REPO" =~ / ]]; then
BASE_REPO="${BASE_REPO#*/}"
fi
fi
fi

echo " Comparing '$BASE_REPO' with allowed '$allowed_repo'"
if [[ "$BASE_REPO" == "$allowed_repo" ]] || [[ "$REPO" == "$allowed_repo" ]]; then
REPO_ALLOWED=true
echo " ✓ Match found"
break
fi
done

if [ "$REPO_ALLOWED" = false ]; then
echo "❌ Repository not allowed: $REPO"
echo "❌ Repository validation failed"
echo " Repository: $REPO"
echo " REASON: Repository is not in the allowlist"
echo " Allowed repositories: $ALLOWED_REPOS"
echo "false" > /tmp/validation_failed.txt
continue
Expand All @@ -414,21 +464,79 @@ jobs:

# 4. Validate tag matches version pattern
if ! [[ "$TAG" =~ $VERSION_PATTERN ]]; then
echo "❌ Tag does not match version pattern: $TAG"
echo " Expected pattern: $VERSION_PATTERN"
echo " This typically means: date-based versions YY.MM.DD where YY >= 25"
echo "❌ Version pattern validation failed"
echo " Tag: $TAG"
echo " Required pattern: $VERSION_PATTERN"
echo " REASON: Only evergreen date-based versions are allowed"
echo " Accepted formats: YY.MM.DD, YY.MM.DD-N, YY.MM.DD_hash, YY.MM.DD-N_hash (where YY >= 25)"
echo "false" > /tmp/validation_failed.txt
continue
fi
echo "✅ Tag matches version pattern"

# 5. Verify image exists in registry (optional)
# 4.5. Anti-downgrade validation (compare versions)
# Get the corresponding old image by index
OLD_IMAGE="${OLD_IMAGES_ARRAY[$INDEX]}"
if [ -n "$OLD_IMAGE" ]; then
# Extract old tag
OLD_TAG="${OLD_IMAGE##*:}"
echo ""
echo "Checking for downgrades..."
echo " Old tag: $OLD_TAG"
echo " New tag: $TAG"

# Extract base version for comparison
# Handle formats: YY.MM.DD, YY.MM.DD-N, YY.MM.DD_hash, YY.MM.DD-N_hash
# First remove hash (everything after underscore)
OLD_VERSION_NO_HASH="${OLD_TAG%%_*}"
NEW_VERSION_NO_HASH="${TAG%%_*}"

# Then remove rebuild number (everything after first dash)
OLD_VERSION="${OLD_VERSION_NO_HASH%%-*}"
NEW_VERSION="${NEW_VERSION_NO_HASH%%-*}"

# Compare versions (YY.MM.DD format)
# Convert to comparable format: YYMMDD
OLD_VER_NUM=$(echo "$OLD_VERSION" | tr -d '.')
NEW_VER_NUM=$(echo "$NEW_VERSION" | tr -d '.')

if [ "$NEW_VER_NUM" -lt "$OLD_VER_NUM" ]; then
echo "❌ Downgrade detected"
echo " Old version: $OLD_VERSION"
echo " New version: $NEW_VERSION"
echo " REASON: Downgrades are not permitted (new version must be >= old version)"
echo "false" > /tmp/validation_failed.txt
continue
elif [ "$NEW_VER_NUM" -eq "$OLD_VER_NUM" ]; then
echo "✅ Same version (suffix may differ): $OLD_VERSION → $NEW_VERSION"
else
echo "✅ Version upgrade: $OLD_VERSION → $NEW_VERSION"
fi
else
echo "ℹ️ No old image found for comparison (new deployment or first validation)"
fi

# Increment index for next iteration
((INDEX++))

# 5. Verify image exists in Docker Hub (canonical registry)
if [ "$VERIFY_EXISTENCE" = "true" ]; then
echo "Verifying image exists in registry..."
if docker manifest inspect "$image" >/dev/null 2>&1; then
echo "✅ Image exists in registry"
# Use canonical image (without registry prefix) to verify in Docker Hub
# This assumes mirror registries have the same images as Docker Hub
CANONICAL_IMAGE="${BASE_REPO}:${TAG}"

echo "Verifying image exists in Docker Hub (canonical)..."
echo " Canonical image: $CANONICAL_IMAGE"

if docker manifest inspect "$CANONICAL_IMAGE" >/dev/null 2>&1; then
echo "✅ Image exists in Docker Hub"
if [ "$REPO" != "$BASE_REPO" ]; then
echo " Note: Assuming mirror registry ($REPO) has the same image"
fi
else
echo "❌ Image does not exist in registry: $image"
echo "❌ Registry existence validation failed"
echo " Canonical image: $CANONICAL_IMAGE"
echo " REASON: Image does not exist in Docker Hub"
echo "false" > /tmp/validation_failed.txt
continue
fi
Expand Down Expand Up @@ -511,40 +619,46 @@ jobs:

# Check file allowlist (if enabled)
if [ "$ENABLE_FILE_ALLOWLIST" = "true" ] && [ "$FILES_CHECK" != "pass" ]; then
echo "❌ BLOCKED: Modified files are not in the allowlist"
echo "❌ BLOCKED: File allowlist validation failed"
echo ""
echo "Only the following files can be modified:"
echo " - ${{ inputs.allowed_files_pattern }}"
echo "REASON: One or more modified files are not in the allowlist"
echo ""
echo "Validation Rule: Only files matching the following pattern can be modified:"
echo " - Pattern: ${{ inputs.allowed_files_pattern }}"
echo ""
echo "Please ensure you're only modifying allowed files."
exit 1
fi

# Check image-only changes (if enabled)
if [ "$ENABLE_IMAGE_ONLY" = "true" ] && [ "$IMAGE_ONLY_CHECK" != "pass" ]; then
echo "❌ BLOCKED: Changes detected beyond the image field"
echo "❌ BLOCKED: Image-only validation failed"
echo ""
echo "REASON: Changes detected beyond the container image field"
echo ""
echo "Only the container image field can be modified."
echo "No other changes are allowed (resources, env vars, volumes, etc.)"
echo "Validation Rule: Only the container image attribute can be modified"
echo " - Allowed: Changes to container image field only"
echo " - Not allowed: resources, env vars, volumes, replicas, or any other configuration changes"
echo ""
echo "Please revert any non-image changes and try again."
exit 1
fi

# Check image validation (if enabled)
if [ "$ENABLE_IMAGE_VALIDATION" = "true" ] && [ "$IMAGE_VALIDATION" != "pass" ]; then
echo "❌ BLOCKED: Image validation failed"
echo ""
echo "Image validation requirements:"
echo "REASON: The specified image does not meet one or more validation requirements"
echo ""
echo "Validation Rules:"
if [ -n "${{ inputs.allowed_image_repositories }}" ]; then
echo " - Repository must be: ${{ inputs.allowed_image_repositories }}"
echo " - Repository: Only images from '${{ inputs.allowed_image_repositories }}' are allowed"
fi
echo " - Tag must match pattern: ${{ inputs.allowed_version_pattern }}"
echo " - Version format: Must match pattern '${{ inputs.allowed_version_pattern }}'"
echo " (Evergreen date-based versions only: YY.MM.DD, YY.MM.DD-N, YY.MM.DD_hash, YY.MM.DD-N_hash where YY >= 25)"
if [ "${{ inputs.verify_image_existence }}" = "true" ]; then
echo " - Image must exist in the registry"
echo " - Registry existence: Image must exist in the Docker registry"
fi
echo " - No downgrades: Version must be equal to or newer than the current deployed version"
echo ""
echo "Please use a valid image and try again."
exit 1
fi

Expand Down