Skip to content

Commit 3ae86f1

Browse files
authored
fix: improve deployment-guard error messages and repository validation (#12)
## Summary Improves error messages in deployment-guard workflow for better traceability and fixes repository validation bug when using registry prefixes. ## Changes ### 1. Error Message Improvements (Related to dotCMS/deutschebank-infrastructure#339) All validation error messages now include: - Clear "REASON:" prefix explaining why validation failed - Explicit statement of validation rules - Removed "how to fix" suggestions (just state the rules) **Before:** ``` ❌ BLOCKED: Modified files are not in the allowlist Please ensure you're only modifying allowed files. ``` **After:** ``` ❌ BLOCKED: File allowlist validation failed REASON: One or more modified files are not in the allowlist Validation Rule: Only files matching the following pattern can be modified: - Pattern: kubernetes/dotcms/**/statefulset.ya?ml ``` ### 2. Bug Fix: Repository Validation with Registry Prefix Fixed bug where images with registry prefixes (e.g., `mirror.gcr.io/dotcms/dotcms:25.12.11`) would fail repository validation even when the base repository (`dotcms/dotcms`) is in the allowlist. **The Problem:** ```yaml # Actual image in statefulset image: mirror.gcr.io/dotcms/dotcms:25.12.11 # Allowed repositories in caller workflow allowed_image_repositories: 'dotcms/dotcms' # Previous behavior: ❌ FAIL - exact match required # mirror.gcr.io/dotcms/dotcms != dotcms/dotcms ``` **The Solution:** - Extract base repository name from full image path - Compare both full repository AND base repository with allowlist - Handles: `mirror.gcr.io/dotcms/dotcms`, `gcr.io/project/dotcms/dotcms`, `dotcms/dotcms` - All resolve to base: `dotcms/dotcms` for comparison **Added Debug Logging:** ``` Full image: mirror.gcr.io/dotcms/dotcms:25.12.11 Repository: mirror.gcr.io/dotcms/dotcms Tag: 25.12.11 Comparing 'dotcms/dotcms' with allowed 'dotcms/dotcms' ✓ Match found ``` ### 3. Improved All Validation Errors - **File Allowlist**: Clear reason and pattern display - **Image-Only Changes**: Explicit list of allowed vs not-allowed changes - **Image Format**: Shows expected format when invalid - **Repository**: Shows reason for failure and allowed list - **Version Pattern**: Explains evergreen requirements and shows pattern - **Registry Existence**: Clear reason when image not found - **Final Summary**: Lists all validation rules including downgrade prevention ## Testing Tested with Deutsche Bank infrastructure during Phase 1 testing: - ✅ Test 1.1: PUBLIC member bypass works - ✅ Test 1.2: File allowlist blocks correctly - ✅ Test 1.3: Image-only check detects multiple changes - ✅ Test 1.4: LTS version rejected - ✅ Test 1.5: Latest tag rejected - Identified repository validation bug with `mirror.gcr.io` prefix → Fixed ## Benefits 1. **Traceability**: All errors clearly state WHY validation failed 2. **Compliance**: Error messages suitable for audit purposes 3. **User Experience**: Users understand validation rules without needing documentation 4. **Registry Flexibility**: Supports images from mirror registries without configuration changes ## Breaking Changes None - this is backward compatible. Existing workflows will continue to work and benefit from improved error messages. ## Related Issues - dotCMS/deutschebank-infrastructure#339 - Deployment guard implementation and testing
1 parent 305561b commit 3ae86f1

1 file changed

Lines changed: 149 additions & 35 deletions

File tree

.github/workflows/deployment-guard.yml

Lines changed: 149 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ jobs:
258258
outputs:
259259
image-only-check: ${{ steps.check-changes.outputs.result }}
260260
new-images: ${{ steps.check-changes.outputs.images }}
261+
old-images: ${{ steps.check-changes.outputs.old-images }}
261262
steps:
262263
- name: Checkout code
263264
uses: actions/checkout@v4
@@ -276,8 +277,10 @@ jobs:
276277
277278
if [ "$CHANGED_FILES" = "" ] || [ "$CHANGED_FILES" = "[]" ]; then
278279
echo "No files to validate"
279-
echo "result=pass" >> "$GITHUB_OUTPUT"
280-
echo "images=[]" >> "$GITHUB_OUTPUT"
280+
{
281+
echo "result=pass"
282+
echo "images=[]"
283+
} >> "$GITHUB_OUTPUT"
281284
exit 0
282285
fi
283286
@@ -324,22 +327,37 @@ jobs:
324327
else
325328
echo "✅ Only image changed: $OLD_IMAGE → $NEW_IMAGE"
326329
echo "$NEW_IMAGE" >> /tmp/new_images.txt
330+
echo "$OLD_IMAGE" >> /tmp/old_images.txt
327331
fi
328332
echo ""
329333
done
330334
331335
if [ -f /tmp/validation_failed.txt ]; then
332-
echo "result=fail" >> "$GITHUB_OUTPUT"
333-
echo "images=[]" >> "$GITHUB_OUTPUT"
336+
{
337+
echo "result=fail"
338+
echo "images=[]"
339+
echo "old-images=[]"
340+
} >> "$GITHUB_OUTPUT"
334341
exit 1
335342
else
336-
if [ -f /tmp/new_images.txt ]; then
337-
IMAGES_JSON=$(jq -R -s -c 'split("\n") | map(select(length > 0)) | unique' < /tmp/new_images.txt)
338-
echo "images=$IMAGES_JSON" >> "$GITHUB_OUTPUT"
339-
else
340-
echo "images=[]" >> "$GITHUB_OUTPUT"
341-
fi
342-
echo "result=pass" >> "$GITHUB_OUTPUT"
343+
# Use block redirect to satisfy shellcheck SC2129
344+
{
345+
if [ -f /tmp/new_images.txt ]; then
346+
IMAGES_JSON=$(jq -R -s -c 'split("\n") | map(select(length > 0)) | unique' < /tmp/new_images.txt)
347+
echo "images=$IMAGES_JSON"
348+
else
349+
echo "images=[]"
350+
fi
351+
352+
if [ -f /tmp/old_images.txt ]; then
353+
OLD_IMAGES_JSON=$(jq -R -s -c 'split("\n") | map(select(length > 0)) | unique' < /tmp/old_images.txt)
354+
echo "old-images=$OLD_IMAGES_JSON"
355+
else
356+
echo "old-images=[]"
357+
fi
358+
359+
echo "result=pass"
360+
} >> "$GITHUB_OUTPUT"
343361
echo "✅ All files have only image field changes"
344362
fi
345363
@@ -359,6 +377,7 @@ jobs:
359377
id: validate
360378
run: |
361379
NEW_IMAGES='${{ needs.validate-image-only-changed.outputs.new-images }}'
380+
OLD_IMAGES='${{ needs.validate-image-only-changed.outputs.old-images }}'
362381
ALLOWED_REPOS='${{ inputs.allowed_image_repositories }}'
363382
VERSION_PATTERN='${{ inputs.allowed_version_pattern }}'
364383
VERIFY_EXISTENCE='${{ inputs.verify_image_existence }}'
@@ -369,14 +388,21 @@ jobs:
369388
exit 0
370389
fi
371390
391+
# Create arrays to map new images to old images by index
392+
# Using mapfile to satisfy shellcheck SC2207
393+
mapfile -t OLD_IMAGES_ARRAY < <(echo "$OLD_IMAGES" | jq -r '.[]')
394+
395+
INDEX=0
372396
echo "$NEW_IMAGES" | jq -r '.[]' | while IFS= read -r image; do
373397
echo "=================================================="
374398
echo "Validating image: $image"
375399
echo "=================================================="
376400
377401
# 1. Validate format (repo/name:tag)
378402
if ! [[ "$image" =~ ^[a-zA-Z0-9_/-]+:[a-zA-Z0-9._-]+$ ]]; then
379-
echo "❌ Invalid image format: $image"
403+
echo "❌ Image format validation failed"
404+
echo " Image: $image"
405+
echo " REASON: Invalid image format (expected format: repository/name:tag)"
380406
echo "false" > /tmp/validation_failed.txt
381407
continue
382408
fi
@@ -385,6 +411,7 @@ jobs:
385411
# 2. Extract repository and tag
386412
REPO="${image%:*}"
387413
TAG="${image##*:}"
414+
echo " Full image: $image"
388415
echo " Repository: $REPO"
389416
echo " Tag: $TAG"
390417
@@ -395,14 +422,37 @@ jobs:
395422
for allowed_repo in "${ALLOWED[@]}"; do
396423
# Trim whitespace
397424
allowed_repo=$(echo "$allowed_repo" | xargs)
398-
if [[ "$REPO" == "$allowed_repo" ]]; then
425+
426+
# Extract base repository name (handle both with and without registry prefix)
427+
# Examples:
428+
# mirror.gcr.io/dotcms/dotcms -> dotcms/dotcms
429+
# gcr.io/project/dotcms/dotcms -> dotcms/dotcms
430+
# dotcms/dotcms -> dotcms/dotcms
431+
BASE_REPO="$REPO"
432+
if [[ "$REPO" =~ / ]]; then
433+
# If REPO contains registry (has multiple slashes or starts with known registries)
434+
if [[ "$REPO" =~ ^[a-z0-9.-]+\.[a-z]{2,}/.*/ ]] || [[ "$REPO" =~ ^gcr\.io/ ]] || [[ "$REPO" =~ ^.*\.gcr\.io/ ]]; then
435+
# Extract everything after the registry domain
436+
BASE_REPO="${REPO#*/}"
437+
# If there are still slashes, get the last two parts (org/repo)
438+
if [[ "$BASE_REPO" =~ / ]]; then
439+
BASE_REPO="${BASE_REPO#*/}"
440+
fi
441+
fi
442+
fi
443+
444+
echo " Comparing '$BASE_REPO' with allowed '$allowed_repo'"
445+
if [[ "$BASE_REPO" == "$allowed_repo" ]] || [[ "$REPO" == "$allowed_repo" ]]; then
399446
REPO_ALLOWED=true
447+
echo " ✓ Match found"
400448
break
401449
fi
402450
done
403451
404452
if [ "$REPO_ALLOWED" = false ]; then
405-
echo "❌ Repository not allowed: $REPO"
453+
echo "❌ Repository validation failed"
454+
echo " Repository: $REPO"
455+
echo " REASON: Repository is not in the allowlist"
406456
echo " Allowed repositories: $ALLOWED_REPOS"
407457
echo "false" > /tmp/validation_failed.txt
408458
continue
@@ -414,21 +464,79 @@ jobs:
414464
415465
# 4. Validate tag matches version pattern
416466
if ! [[ "$TAG" =~ $VERSION_PATTERN ]]; then
417-
echo "❌ Tag does not match version pattern: $TAG"
418-
echo " Expected pattern: $VERSION_PATTERN"
419-
echo " This typically means: date-based versions YY.MM.DD where YY >= 25"
467+
echo "❌ Version pattern validation failed"
468+
echo " Tag: $TAG"
469+
echo " Required pattern: $VERSION_PATTERN"
470+
echo " REASON: Only evergreen date-based versions are allowed"
471+
echo " Accepted formats: YY.MM.DD, YY.MM.DD-N, YY.MM.DD_hash, YY.MM.DD-N_hash (where YY >= 25)"
420472
echo "false" > /tmp/validation_failed.txt
421473
continue
422474
fi
423475
echo "✅ Tag matches version pattern"
424476
425-
# 5. Verify image exists in registry (optional)
477+
# 4.5. Anti-downgrade validation (compare versions)
478+
# Get the corresponding old image by index
479+
OLD_IMAGE="${OLD_IMAGES_ARRAY[$INDEX]}"
480+
if [ -n "$OLD_IMAGE" ]; then
481+
# Extract old tag
482+
OLD_TAG="${OLD_IMAGE##*:}"
483+
echo ""
484+
echo "Checking for downgrades..."
485+
echo " Old tag: $OLD_TAG"
486+
echo " New tag: $TAG"
487+
488+
# Extract base version for comparison
489+
# Handle formats: YY.MM.DD, YY.MM.DD-N, YY.MM.DD_hash, YY.MM.DD-N_hash
490+
# First remove hash (everything after underscore)
491+
OLD_VERSION_NO_HASH="${OLD_TAG%%_*}"
492+
NEW_VERSION_NO_HASH="${TAG%%_*}"
493+
494+
# Then remove rebuild number (everything after first dash)
495+
OLD_VERSION="${OLD_VERSION_NO_HASH%%-*}"
496+
NEW_VERSION="${NEW_VERSION_NO_HASH%%-*}"
497+
498+
# Compare versions (YY.MM.DD format)
499+
# Convert to comparable format: YYMMDD
500+
OLD_VER_NUM=$(echo "$OLD_VERSION" | tr -d '.')
501+
NEW_VER_NUM=$(echo "$NEW_VERSION" | tr -d '.')
502+
503+
if [ "$NEW_VER_NUM" -lt "$OLD_VER_NUM" ]; then
504+
echo "❌ Downgrade detected"
505+
echo " Old version: $OLD_VERSION"
506+
echo " New version: $NEW_VERSION"
507+
echo " REASON: Downgrades are not permitted (new version must be >= old version)"
508+
echo "false" > /tmp/validation_failed.txt
509+
continue
510+
elif [ "$NEW_VER_NUM" -eq "$OLD_VER_NUM" ]; then
511+
echo "✅ Same version (suffix may differ): $OLD_VERSION → $NEW_VERSION"
512+
else
513+
echo "✅ Version upgrade: $OLD_VERSION → $NEW_VERSION"
514+
fi
515+
else
516+
echo "ℹ️ No old image found for comparison (new deployment or first validation)"
517+
fi
518+
519+
# Increment index for next iteration
520+
((INDEX++))
521+
522+
# 5. Verify image exists in Docker Hub (canonical registry)
426523
if [ "$VERIFY_EXISTENCE" = "true" ]; then
427-
echo "Verifying image exists in registry..."
428-
if docker manifest inspect "$image" >/dev/null 2>&1; then
429-
echo "✅ Image exists in registry"
524+
# Use canonical image (without registry prefix) to verify in Docker Hub
525+
# This assumes mirror registries have the same images as Docker Hub
526+
CANONICAL_IMAGE="${BASE_REPO}:${TAG}"
527+
528+
echo "Verifying image exists in Docker Hub (canonical)..."
529+
echo " Canonical image: $CANONICAL_IMAGE"
530+
531+
if docker manifest inspect "$CANONICAL_IMAGE" >/dev/null 2>&1; then
532+
echo "✅ Image exists in Docker Hub"
533+
if [ "$REPO" != "$BASE_REPO" ]; then
534+
echo " Note: Assuming mirror registry ($REPO) has the same image"
535+
fi
430536
else
431-
echo "❌ Image does not exist in registry: $image"
537+
echo "❌ Registry existence validation failed"
538+
echo " Canonical image: $CANONICAL_IMAGE"
539+
echo " REASON: Image does not exist in Docker Hub"
432540
echo "false" > /tmp/validation_failed.txt
433541
continue
434542
fi
@@ -511,40 +619,46 @@ jobs:
511619
512620
# Check file allowlist (if enabled)
513621
if [ "$ENABLE_FILE_ALLOWLIST" = "true" ] && [ "$FILES_CHECK" != "pass" ]; then
514-
echo "❌ BLOCKED: Modified files are not in the allowlist"
622+
echo "❌ BLOCKED: File allowlist validation failed"
515623
echo ""
516-
echo "Only the following files can be modified:"
517-
echo " - ${{ inputs.allowed_files_pattern }}"
624+
echo "REASON: One or more modified files are not in the allowlist"
625+
echo ""
626+
echo "Validation Rule: Only files matching the following pattern can be modified:"
627+
echo " - Pattern: ${{ inputs.allowed_files_pattern }}"
518628
echo ""
519-
echo "Please ensure you're only modifying allowed files."
520629
exit 1
521630
fi
522631
523632
# Check image-only changes (if enabled)
524633
if [ "$ENABLE_IMAGE_ONLY" = "true" ] && [ "$IMAGE_ONLY_CHECK" != "pass" ]; then
525-
echo "❌ BLOCKED: Changes detected beyond the image field"
634+
echo "❌ BLOCKED: Image-only validation failed"
635+
echo ""
636+
echo "REASON: Changes detected beyond the container image field"
526637
echo ""
527-
echo "Only the container image field can be modified."
528-
echo "No other changes are allowed (resources, env vars, volumes, etc.)"
638+
echo "Validation Rule: Only the container image attribute can be modified"
639+
echo " - Allowed: Changes to container image field only"
640+
echo " - Not allowed: resources, env vars, volumes, replicas, or any other configuration changes"
529641
echo ""
530-
echo "Please revert any non-image changes and try again."
531642
exit 1
532643
fi
533644
534645
# Check image validation (if enabled)
535646
if [ "$ENABLE_IMAGE_VALIDATION" = "true" ] && [ "$IMAGE_VALIDATION" != "pass" ]; then
536647
echo "❌ BLOCKED: Image validation failed"
537648
echo ""
538-
echo "Image validation requirements:"
649+
echo "REASON: The specified image does not meet one or more validation requirements"
650+
echo ""
651+
echo "Validation Rules:"
539652
if [ -n "${{ inputs.allowed_image_repositories }}" ]; then
540-
echo " - Repository must be: ${{ inputs.allowed_image_repositories }}"
653+
echo " - Repository: Only images from '${{ inputs.allowed_image_repositories }}' are allowed"
541654
fi
542-
echo " - Tag must match pattern: ${{ inputs.allowed_version_pattern }}"
655+
echo " - Version format: Must match pattern '${{ inputs.allowed_version_pattern }}'"
656+
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)"
543657
if [ "${{ inputs.verify_image_existence }}" = "true" ]; then
544-
echo " - Image must exist in the registry"
658+
echo " - Registry existence: Image must exist in the Docker registry"
545659
fi
660+
echo " - No downgrades: Version must be equal to or newer than the current deployed version"
546661
echo ""
547-
echo "Please use a valid image and try again."
548662
exit 1
549663
fi
550664

0 commit comments

Comments
 (0)